keuss 1.6.15 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Queue.js CHANGED
@@ -98,6 +98,9 @@ class Queue {
98
98
  // remove (and return if possible) by id
99
99
  remove (id, callback) {callback(null, null);}
100
100
 
101
+ // extra information & status
102
+ extra_info (cb) {cb (null, {});}
103
+
101
104
  // end of expected redefinitions on subclasses
102
105
  ////////////////////////////////////////////////////////////////////////////
103
106
 
@@ -113,6 +116,29 @@ class Queue {
113
116
  capabilities () {
114
117
  return this._factory.capabilities ();
115
118
  }
119
+
120
+
121
+ info (cb) {
122
+ async.parallel ({
123
+ size: cb => this.size (cb),
124
+ totalSize: cb => this.totalSize (cb),
125
+ schedSize: cb => this.schedSize (cb),
126
+ resvSize: cb => this.resvSize (cb),
127
+ next_t: cb => this.next_t (cb),
128
+ stats: cb => this.stats (cb),
129
+ paused: cb => this.paused (cb),
130
+ extra: cb => this.extra_info (cb),
131
+ }, (err, res) => {
132
+ if (err) return cb (err);
133
+
134
+ res.name = this.name();
135
+ res.ns = this.ns();
136
+ res.type = this.type();
137
+ res.capabilities = this.capabilities();
138
+
139
+ cb (null, res);
140
+ });
141
+ }
116
142
 
117
143
  // T of next mature
118
144
  nextMatureDate () {return this._next_mature_t;}
package/README.md CHANGED
@@ -5,3 +5,4 @@ Enterprise-grade Job Queues for node.js, backed by redis and/or MongoDB
5
5
  * [Documentation](https://pepmartinez.github.io/keuss/docs/)
6
6
  * [Examples](https://pepmartinez.github.io/keuss/docs/examples)
7
7
  * [Changelog](https://pepmartinez.github.io/keuss/docs/changelog)
8
+
@@ -14,6 +14,8 @@ class PersistentMongoQueue extends Queue {
14
14
  constructor (name, factory, opts, orig_opts) {
15
15
  super (name, factory, opts, orig_opts);
16
16
 
17
+ if (!this._opts.ttl) this._opts.ttl = 3600;
18
+
17
19
  this._factory = factory;
18
20
  this._col = factory._db.collection (name);
19
21
  this.ensureIndexes (function (err) {});
@@ -50,7 +52,10 @@ class PersistentMongoQueue extends Queue {
50
52
  };
51
53
 
52
54
  var updt = {
53
- $set: {processed: new Date ()}
55
+ $set: {
56
+ processed: new Date (),
57
+ mature: Queue.nowPlusSecs (100 * this._opts.ttl)
58
+ }
54
59
  };
55
60
 
56
61
  var opts = {
@@ -78,7 +83,10 @@ class PersistentMongoQueue extends Queue {
78
83
  };
79
84
 
80
85
  var update = {
81
- $set: {mature: Queue.nowPlusSecs (delay), reserved: new Date ()},
86
+ $set: {
87
+ mature: Queue.nowPlusSecs (delay),
88
+ reserved: new Date ()
89
+ },
82
90
  $inc: {tries: 1}
83
91
  };
84
92
 
@@ -113,7 +121,10 @@ class PersistentMongoQueue extends Queue {
113
121
  }
114
122
 
115
123
  var updt = {
116
- $set: {processed: new Date ()},
124
+ $set: {
125
+ processed: new Date (),
126
+ mature: Queue.nowPlusSecs (100 * this._opts.ttl)
127
+ },
117
128
  $unset: {reserved: ''}
118
129
  };
119
130
 
@@ -226,7 +237,11 @@ class PersistentMongoQueue extends Queue {
226
237
  }
227
238
 
228
239
  var updt = {
229
- $set: {processed: new Date (), removed: true},
240
+ $set: {
241
+ processed: new Date (),
242
+ mature: Queue.nowPlusSecs (100 * this._opts.ttl),
243
+ removed: true
244
+ },
230
245
  };
231
246
 
232
247
  var opts = {};
@@ -261,7 +276,7 @@ class PersistentMongoQueue extends Queue {
261
276
  ensureIndexes (cb) {
262
277
  this._col.createIndex ({mature : 1}, err => {
263
278
  if (err) return cb (err);
264
- this._col.createIndex({processed: 1}, {expireAfterSeconds: this._opts.ttl || 3600}, err => cb (err));
279
+ this._col.createIndex({processed: 1}, {expireAfterSeconds: this._opts.ttl}, err => cb (err));
265
280
  });
266
281
  }
267
282
  }
@@ -0,0 +1,476 @@
1
+ const _ = require ('lodash');
2
+ const async = require ('async');
3
+
4
+ const MongoClient = require ('mongodb').MongoClient;
5
+ const mongo = require ('mongodb');
6
+
7
+ const Queue = require ('../Queue');
8
+ const QFactory_MongoDB_defaults = require ('../QFactory-MongoDB-defaults');
9
+
10
+ var debug = require('debug')('keuss:Queue:StreamMongo');
11
+
12
+ class StreamMongoQueue extends Queue {
13
+
14
+ //////////////////////////////////////////////
15
+ constructor (name, factory, opts, orig_opts) {
16
+ super (name, factory, opts, orig_opts);
17
+
18
+ if (!this._opts.ttl) this._opts.ttl = 3600;
19
+
20
+ this._factory = factory;
21
+ this._col = factory._db.collection (name);
22
+ this._groups_str = this._opts.groups || 'A,B:C';
23
+ this._groups_vector = this._groups_str.split (/[:,;.-]/).map (i => i.trim());
24
+ this._gid = this._opts.group || this._groups_vector[0];
25
+
26
+ this.ensureIndexes (err => {
27
+ if (err) {
28
+ console.error ('keuss:Queue:StreamMongo: index creation failed, queues performance will be severely impacted:', err);
29
+ }
30
+ else {
31
+ debug ('indexes created');
32
+ }
33
+ });
34
+
35
+ debug ('created with groups %j and gid %s (used for pop/reserve only)', this._groups_vector, this._gid);
36
+ }
37
+
38
+
39
+ /////////////////////////////////////////
40
+ static Type () {
41
+ return 'mongo:stream';
42
+ }
43
+
44
+
45
+ /////////////////////////////////////////
46
+ type () {
47
+ return 'mongo:stream';
48
+ }
49
+
50
+
51
+ /////////////////////////////////////////
52
+ _vector (item) {
53
+ const r = {};
54
+ this._groups_vector.forEach (i => r[i] = item);
55
+ return r;
56
+ }
57
+
58
+
59
+ /////////////////////////////////////////
60
+ // add element to queue
61
+ insert (entry, callback) {
62
+ const mtr = entry.mature;
63
+ const tr = entry.tries;
64
+
65
+ entry.tries = this._vector (tr);
66
+ entry.mature = this._vector (mtr);
67
+ entry.processed = this._vector (false);
68
+
69
+ entry.t = new Date();
70
+
71
+ this._col.insertOne (entry, {}, (err, result) => {
72
+ if (err) return callback (err);
73
+ // TODO result.insertedCount must be 1
74
+ callback (null, result.insertedId);
75
+ this._groups_vector.forEach (i => this._stats.incr (`stream.${i}.put`));
76
+ });
77
+ }
78
+
79
+
80
+ /////////////////////////////////////////
81
+ // get element from queue
82
+ get (callback) {
83
+ const gid = this._gid;
84
+ const q = {};
85
+
86
+ q[`mature.${gid}`] = {$lte: Queue.nowPlusSecs (0)};
87
+ q[`processed.${gid}`] = false;
88
+
89
+ const updt = {
90
+ $set: {}
91
+ };
92
+ updt.$set[`processed.${gid}`] = new Date ();
93
+ updt.$set[`mature.${gid}`] = Queue.nowPlusSecs (100 * this._opts.ttl);
94
+
95
+ const opts = {
96
+ sort: {}
97
+ };
98
+ opts.sort[`mature.${gid}`] = 1;
99
+
100
+ debug ('get() with q %O, upd %O, opts %o', q, updt, opts);
101
+
102
+ this._col.findOneAndUpdate (q, updt, opts, (err, result) => {
103
+ if (err) return callback (err);
104
+ const v = result && result.value;
105
+ if (!v) return callback ();
106
+ if (v.payload._bsontype == 'Binary') v.payload = v.payload.buffer;
107
+ v.mature = v.mature[gid];
108
+ v.tries = v.tries[gid];
109
+ delete v.processed;
110
+ delete v.t;
111
+ callback (null, v);
112
+ this._stats.incr (`stream.${gid}.get`);
113
+ });
114
+ }
115
+
116
+
117
+ //////////////////////////////////
118
+ // reserve element: call cb (err, pl) where pl has an id
119
+ reserve (callback) {
120
+ const gid = this._gid;
121
+ const delay = this._opts.reserve_delay || 120;
122
+
123
+ const q = {};
124
+
125
+ q[`mature.${gid}`] = {$lte: Queue.nowPlusSecs (0)};
126
+ q[`processed.${gid}`] = false;
127
+
128
+ const updt = {
129
+ $set: {},
130
+ $inc: {}
131
+ };
132
+ updt.$set[`reserved.${gid}`] = new Date ();
133
+ updt.$set[`mature.${gid}`] = Queue.nowPlusSecs (delay);
134
+ updt.$inc[`tries.${gid}`] = 1;
135
+
136
+ const opts = {
137
+ sort: {},
138
+ returnDocument: 'before'
139
+ };
140
+ opts.sort[`mature.${gid}`] = 1;
141
+
142
+ debug ('reserve() with q %O, upd %O, opts %o', q, updt, opts);
143
+
144
+ this._col.findOneAndUpdate (q, updt, opts, (err, result) => {
145
+ if (err) return callback (err);
146
+ const v = result && result.value;
147
+ if (!v) return callback ();
148
+ if (v.payload._bsontype == 'Binary') v.payload = v.payload.buffer;
149
+ v.mature = v.mature[gid];
150
+ v.tries = v.tries[gid];
151
+ delete v.processed;
152
+ delete v.t;
153
+ callback (null, v);
154
+ this._stats.incr (`stream.${gid}.reserve`);
155
+ });
156
+ }
157
+
158
+
159
+ //////////////////////////////////
160
+ // commit previous reserve, by p.id
161
+ commit (id, callback) {
162
+ const gid = this._gid;
163
+ let q;
164
+
165
+ try {
166
+ q = {
167
+ _id: (_.isString(id) ? new mongo.ObjectID (id) : id),
168
+ };
169
+ q[`reserved.${gid}`] = {$exists: true};
170
+ }
171
+ catch (e) {
172
+ return callback ('id [' + id + '] can not be used as rollback id: ' + e);
173
+ }
174
+
175
+ const updt = {
176
+ $set: {},
177
+ $unset: {}
178
+ };
179
+ updt.$set[`processed.${gid}`] = new Date ();
180
+ updt.$set[`mature.${gid}`] = Queue.nowPlusSecs (100 * this._opts.ttl);
181
+ updt.$unset[`reserved.${gid}`] = '';
182
+
183
+ const opts = {};
184
+
185
+ debug ('commit() with q %O, upd %O, opts %o', q, updt, opts);
186
+
187
+ this._col.updateOne (q, updt, opts, (err, result) => {
188
+ if (err) return callback (err);
189
+ callback (null, result && (result.modifiedCount == 1));
190
+ this._stats.incr (`stream.${gid}.commit`);
191
+ });
192
+ }
193
+
194
+
195
+ //////////////////////////////////
196
+ // rollback previous reserve, by p.id
197
+ rollback (id, next_t, callback) {
198
+ const gid = this._gid;
199
+ let q;
200
+
201
+ if (_.isFunction (next_t)) {
202
+ callback = next_t;
203
+ next_t = null;
204
+ }
205
+
206
+ try {
207
+ q = {
208
+ _id: (_.isString(id) ? new mongo.ObjectID (id) : id),
209
+ };
210
+ q[`reserved.${gid}`] = {$exists: true};
211
+ }
212
+ catch (e) {
213
+ return callback ('id [' + id + '] can not be used as rollback id: ' + e);
214
+ }
215
+
216
+ const updt = {
217
+ $set: {},
218
+ $unset: {}
219
+ };
220
+ updt.$set[`mature.${gid}`] = (next_t ? new Date (next_t) : Queue.now ());
221
+ updt.$unset[`reserved.${gid}`] = '';
222
+
223
+ const opts = {};
224
+
225
+ debug ('rollback() with q %O, upd %O, opts %o', q, updt, opts);
226
+
227
+ this._col.updateOne (q, updt, opts, (err, result) => {
228
+ if (err) return callback (err);
229
+ callback (null, result && (result.modifiedCount == 1));
230
+ this._stats.incr (`stream.${gid}.rollback`);
231
+ });
232
+ }
233
+
234
+
235
+ //////////////////////////////////
236
+ // queue size including non-mature elements
237
+ totalSize (callback, gid) {
238
+ const gr = gid || this._gid;
239
+
240
+ const q = {};
241
+ q[`processed.${gr}`] = false;
242
+
243
+ const opts = {};
244
+ this._col.countDocuments (q, opts, callback);
245
+ }
246
+
247
+
248
+ //////////////////////////////////
249
+ // queue size NOT including non-mature elements
250
+ size (callback, gid) {
251
+ const gr = gid || this._gid;
252
+
253
+ const q = {};
254
+ q[`processed.${gr}`] = false;
255
+ q[`mature.${gr}`] = {$lte: Queue.now()};
256
+
257
+ const opts = {};
258
+ this._col.countDocuments (q, opts, callback);
259
+ }
260
+
261
+
262
+ //////////////////////////////////
263
+ // queue size of non-mature elements only
264
+ schedSize (callback, gid) {
265
+ const gr = gid || this._gid;
266
+
267
+ const q = {};
268
+ q[`processed.${gr}`] = false;
269
+ q[`reserved.${gr}`] = {$exists: false};
270
+ q[`mature.${gr}`] = {$gt: Queue.now()};
271
+
272
+ const opts = {};
273
+ this._col.countDocuments (q, opts, callback);
274
+ }
275
+
276
+
277
+ //////////////////////////////////
278
+ // queue size of reserved elements only
279
+ resvSize (callback, gid) {
280
+ const gr = gid || this._gid;
281
+
282
+ const q = {};
283
+ q[`processed.${gr}`] = false;
284
+ q[`reserved.${gr}`] = {$exists: true};
285
+ q[`mature.${gr}`] = {$gt: Queue.now()};
286
+
287
+ const opts = {};
288
+ this._col.countDocuments (q, opts, callback);
289
+ }
290
+
291
+
292
+ /////////////////////////////////////////
293
+ // get element from queue
294
+ next_t (callback, gid) {
295
+ const gr = gid || this._gid;
296
+
297
+ const q = {};
298
+ q[`processed.${gr}`] = false;
299
+
300
+ const sort = {};
301
+ sort[`mature.${gr}`] = 1;
302
+
303
+ this._col
304
+ .find (q)
305
+ .limit(1)
306
+ .sort (sort)
307
+ .project ({mature:1})
308
+ .next ((err, result) => {
309
+ if (err) return callback (err);
310
+ debug ('next_t with git %s: got %o', gr, result);
311
+ callback (null, result && result.mature && result.mature[gr]);
312
+ });
313
+ }
314
+
315
+
316
+ //////////////////////////////////
317
+ // queue size of non-mature elements only.
318
+ // COMMENTED OUT: takes 1 sec per each 100K elements in collection
319
+ /*
320
+ extra_info (callback) {
321
+ const cursor = this._col.aggregate ([
322
+ {
323
+ $group : {
324
+ _id : "v",
325
+ r: {
326
+ $accumulator: {
327
+ init: `function() {
328
+ return { size: {}, totalSize: {}, resvSize: {}, schedSize: {}, processed: {} }
329
+ }`,
330
+ accumulate: `function(state, mature, reserved, processed) {
331
+ for (const gr in mature) {
332
+ if (!state.size[gr]) state.size[gr] = 0;
333
+ if (!state.totalSize[gr]) state.totalSize[gr] = 0;
334
+ if (!state.resvSize[gr]) state.resvSize[gr] = 0;
335
+ if (!state.schedSize[gr]) state.schedSize[gr] = 0;
336
+ if (!state.processed[gr]) state.processed[gr] = 0;
337
+
338
+ const mtr = (mature[gr].getTime() < new Date().getTime());
339
+ const rsv = (reserved && ((reserved[gr] != null) && (reserved[gr] != undefined)));
340
+ const prc = processed[gr];
341
+
342
+ if (prc) {
343
+ state.processed[gr]++;
344
+ }
345
+ else {
346
+ state.totalSize[gr]++;
347
+
348
+ if (mtr) {
349
+ state.size[gr]++;
350
+ }
351
+ else {
352
+ if (rsv) state.resvSize[gr]++;
353
+ else state.schedSize[gr]++;
354
+ }
355
+ }
356
+ }
357
+
358
+ return state;
359
+ }`,
360
+ accumulateArgs: ['$mature', '$reserved', '$processed'],
361
+ merge: `function(state1, state2) {
362
+ const res = { size: {}, totalSize: {}, resvSize: {}, schedSize: {} }
363
+ for (const gr in state1) {
364
+ res.size[gr] = state1.size[gr] + (state2.size[gr] || 0)
365
+ res.totalSize[gr] = state1.totalSize[gr] + (state2.totalSize[gr] || 0)
366
+ res.resvSize[gr] = state1.resvSize[gr] + (state2.resvSize[gr] || 0)
367
+ res.schedSize[gr] = state1.schedSize[gr] + (state2.schedSize[gr] || 0)
368
+ res.processed[gr] = state1.processed[gr] + (state2.processed[gr] || 0)
369
+ }
370
+ for (const gr in state2) {
371
+ if (!state1.size[gr]) res.size[gr] = state2.size[gr]
372
+ if (!state1.totalSize[gr]) res.totalSize[gr] = state2.totalSize[gr]
373
+ if (!state1.schedSize[gr]) res.schedSize[gr] = state2.schedSize[gr]
374
+ if (!state1.resvSize[gr]) res.resvSize[gr] = state2.resvSize[gr]
375
+ if (!state1.processed[gr]) res.processed[gr] = state2.processed[gr]
376
+ }
377
+ return res
378
+ }`,
379
+ lang: "js"
380
+ }
381
+ }
382
+ }
383
+ }
384
+
385
+ ]);
386
+
387
+
388
+
389
+
390
+ cursor.toArray ((err, res) => {
391
+ debug ('calculating resvSize: aggregation pipeline returns %o', err);
392
+ debug ('calculating resvSize: aggregation pipeline returns %o', res);
393
+ if (err) return callback (err);
394
+ if (res.length == 0) return callback (null, 0);
395
+ callback (null, res[0].r);
396
+ });
397
+ }
398
+ */
399
+
400
+
401
+ ///////////////////////////////////////////////////////////////////////////////
402
+ // private parts
403
+
404
+ //////////////////////////////////////////////////////////////////
405
+ // create needed indexes for O(1) functioning
406
+ ensureIndexes (cb) {
407
+ const tasks = [];
408
+
409
+ this._groups_vector.forEach (i => {
410
+ const idx = {};
411
+ idx[`mature.${i}`] = 1;
412
+ tasks.push (cb => this._col.createIndex (idx, cb));
413
+ });
414
+ tasks.push (cb => this._col.createIndex ({t: 1}, {expireAfterSeconds: this._opts.ttl}, cb));
415
+ async.series (tasks, cb);
416
+ }
417
+ }
418
+
419
+
420
+ class Factory extends QFactory_MongoDB_defaults {
421
+ constructor (opts, mongo_conn) {
422
+ super (opts);
423
+ this._mongo_conn = mongo_conn;
424
+ this._db = mongo_conn.db();
425
+ }
426
+
427
+ queue (name, opts) {
428
+ const full_opts = {};
429
+ _.merge(full_opts, this._opts, opts);
430
+ return new StreamMongoQueue (name, this, full_opts, opts);
431
+ }
432
+
433
+ close (cb) {
434
+ super.close (() => {
435
+ if (this._mongo_conn) {
436
+ this._mongo_conn.close ();
437
+ this._mongo_conn = null;
438
+ }
439
+
440
+ if (cb) return cb ();
441
+ });
442
+ }
443
+
444
+ type () {
445
+ return StreamMongoQueue.Type ();
446
+ }
447
+
448
+ capabilities () {
449
+ return {
450
+ sched: true,
451
+ reserve: true,
452
+ pipeline: false,
453
+ tape: true,
454
+ remove: false,
455
+ stream: true
456
+ };
457
+ }
458
+ }
459
+
460
+ function creator (opts, cb) {
461
+ const _opts = opts || {};
462
+ const m_url = _opts.url || 'mongodb://localhost:27017/keuss';
463
+
464
+ MongoClient.connect (m_url, { useNewUrlParser: true }, (err, cl) => {
465
+ if (err) return cb (err);
466
+ const F = new Factory (_opts, cl);
467
+ F.async_init (err => cb (null, F));
468
+ });
469
+ }
470
+
471
+ module.exports = creator;
472
+
473
+
474
+
475
+
476
+
@@ -0,0 +1,53 @@
1
+ /*
2
+ *
3
+ * very simple example of stream-mongo: one element pushed, consumed three times
4
+ *
5
+ */
6
+
7
+ const async = require ('async');
8
+ const MQ = require ('../../backends/stream-mongo');
9
+
10
+ // initialize factory
11
+ MQ ({
12
+ url: 'mongodb://localhost/keuss_test_stream'
13
+ }, (err, factory) => {
14
+ if (err) return console.error(err);
15
+
16
+ // factory ready, create one queue
17
+ const q0 = factory.queue ('test_stream', {groups: 'G1, G2, G4'});
18
+ const q1 = factory.queue ('test_stream', {group: 'G1'});
19
+ const q2 = factory.queue ('test_stream', {group: 'G2'});
20
+
21
+ async.series ([
22
+ // push element
23
+ cb => q0.push (
24
+ {elem: 1, headline: 'something something', tags: {a: 1, b: 2}}, // this is the payload
25
+ {
26
+ hdrs: {h1: 'aaa', h2: 12, h3: false} // let's add some headers too
27
+ },
28
+ cb
29
+ ),
30
+ cb => setTimeout (cb, 1000), // wait a bit
31
+ cb => q1.pop ('consumer-1', cb), // pop element in group G1
32
+ cb => q2.pop ('consumer-2', cb), // pop element in group G2
33
+ ], (err, res) => {
34
+ if (err) {
35
+ console.error (err);
36
+ }
37
+ else {
38
+ console.log ('element popped for group G1:', res[2]);
39
+ console.log ('element popped for group G2:', res[3]);
40
+ // this should print twice something like:
41
+ // {
42
+ // _id: <some id>,
43
+ // mature: <some date>,
44
+ // payload: { elem: 1, headline: 'something something', tags: { a: 1, b: 2 } },
45
+ // tries: 0
46
+ // hdrs: {h1: 'aaa', h2: 12, h3: false}
47
+ // }
48
+ }
49
+
50
+ factory.close ();
51
+ });
52
+ });
53
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keuss",
3
- "version": "1.6.15",
3
+ "version": "1.7.1",
4
4
  "keywords": [
5
5
  "queue",
6
6
  "persistent",