keuss 1.7.1 → 1.7.2

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.
@@ -11,7 +11,7 @@ class QFactory_MongoDB_defaults extends QFactory {
11
11
  super (opts);
12
12
 
13
13
  if (!this._opts.url) {
14
- this._opts.url = 'http://localhost:27017/keuss';
14
+ this._opts.url = 'mongodb://localhost:27017/keuss';
15
15
  debug ('added url default to %s: %o', this._name, this._opts.url);
16
16
  }
17
17
 
package/Queue.js CHANGED
@@ -443,7 +443,7 @@ class Queue {
443
443
  }
444
444
 
445
445
  cb (err, res);
446
- });
446
+ }, obj);
447
447
  }
448
448
 
449
449
 
@@ -765,7 +765,7 @@ class Queue {
765
765
  return cb (null, 'deadletter');
766
766
  }
767
767
  });
768
- });
768
+ }, obj);
769
769
  }
770
770
  }
771
771
 
package/TODO CHANGED
@@ -8,6 +8,14 @@ prio 1
8
8
  * pipelines
9
9
  + add some commonplace transforms (etl-like)
10
10
  + create a server for that, allow processor code defined externally
11
+ * documentation
12
+ + blog entries:
13
+ - motivations on using mongodb+redis. comparatives, pros&cons
14
+ - buckets
15
+ - pipelines
16
+ - tape
17
+ - redis-oq
18
+ - exotic experiments: intraorder
11
19
 
12
20
  prio 0
13
21
  -----------------------------------------------------------------
@@ -0,0 +1,374 @@
1
+ const _ = require ('lodash');
2
+ const async = require ('async');
3
+ const uuid = require ('uuid');
4
+
5
+ const MongoClient = require ('mongodb').MongoClient;
6
+ const mongo = require ('mongodb');
7
+
8
+ const Queue = require ('../Queue');
9
+ const QFactory_MongoDB_defaults = require ('../QFactory-MongoDB-defaults');
10
+
11
+ const debug = require('debug')('keuss:Queue:intraqueue');
12
+
13
+
14
+ class IntraOrderedQueue extends Queue {
15
+
16
+ //////////////////////////////////////////////
17
+ constructor (name, factory, opts, orig_opts) {
18
+ super (name, factory, opts, orig_opts);
19
+
20
+ this._factory = factory;
21
+ this._col = factory._db.collection (name);
22
+ this.ensureIndexes (err => {});
23
+ }
24
+
25
+
26
+ /////////////////////////////////////////
27
+ static Type () {
28
+ return 'mongo:intraorder';
29
+ }
30
+
31
+
32
+ /////////////////////////////////////////
33
+ type () {
34
+ return 'mongo:intraorder';
35
+ }
36
+
37
+
38
+ /////////////////////////////////////////
39
+ // add element to queue
40
+ insert (entry, callback) {
41
+ const q = { _id: entry.payload.iid || uuid.v4 () };
42
+ const upd = {
43
+ $push: { q: entry },
44
+ $inc: { qcnt: 1 },
45
+ $set: { mature: entry.mature, tries: entry.tries },
46
+ };
47
+ const opts = { upsert: true };
48
+
49
+ this._col.updateOne (q, upd, opts, (err, result) => {
50
+ debug ('insert: updateOne (%j, %j, %j) => (%j, %j)', q, upd, opts, err, result);
51
+ if (err) return callback (err);
52
+ // TODO result.insertedCount must be 1
53
+ callback (null, result.upsertedId || q._id);
54
+ });
55
+ }
56
+
57
+
58
+ /////////////////////////////////////////
59
+ // get element from queue
60
+ get (callback) {
61
+ // actually, a reserve followed by a commit
62
+ this.reserve ((err, elem) => {
63
+ if (err) return callback (err);
64
+ if (!elem) return callback ();
65
+
66
+ this.commit (elem._id, (err, res) => {
67
+ if (err) return callback (err);
68
+
69
+ // clear _env: not needed here
70
+ delete elem._env;
71
+ callback (null, elem);
72
+ }, elem);
73
+ })
74
+ }
75
+
76
+
77
+ //////////////////////////////////
78
+ // reserve element: call cb (err, pl) where pl has an id
79
+ reserve (callback) {
80
+ const delay = this._opts.reserve_delay || 120;
81
+
82
+ const q = {
83
+ mature: {$lte: Queue.nowPlusSecs (0)},
84
+ qcnt: { $gt: 0}
85
+ };
86
+
87
+ const upd = {
88
+ $set: {
89
+ mature: Queue.nowPlusSecs (delay),
90
+ reserved: new Date ()
91
+ },
92
+ $inc: {tries: 1}
93
+ };
94
+
95
+ const opts = {
96
+ sort: {mature : 1},
97
+ returnDocument: 'before'
98
+ };
99
+
100
+ this._col.findOneAndUpdate (q, upd, opts, (err, result) => {
101
+ debug ('reserve: findOneAndUpdate (%j, %j, %j) => (%j, %j)', q, upd, opts, err, result);
102
+ if (err) return callback (err);
103
+ const v = result && result.value;
104
+ if (!v) return callback ();
105
+
106
+ // construct the real object to return
107
+ const vq = v.q[0];
108
+ vq._id = v._id; // use the whole obj's _id
109
+ vq.tries = v.tries
110
+ vq._env = v; // pass along the whole obj too
111
+ if (vq.payload._bsontype == 'Binary') vq.payload = vq.payload.buffer;
112
+ callback (null, vq);
113
+ });
114
+ }
115
+
116
+
117
+ //////////////////////////////////
118
+ // commit previous reserve, by p.id
119
+ commit (id, callback, obj) {
120
+ if (!(obj || (obj && obj._id))) {
121
+ // full obj must be passed to commit
122
+ return callback ('full obj must be passed to commit');
123
+ }
124
+
125
+ const q = {
126
+ _id: id,
127
+ reserved: {$exists: true}
128
+ };
129
+
130
+ const upd = {
131
+ // do not alter mature on commit: leave it be
132
+ // $set: {
133
+ // mature: Queue.nowPlusSecs (100 * this._opts.ttl)
134
+ // },
135
+ $pop: {q: -1}, // pop entry from queue
136
+ $inc: { qcnt: -1 }, // one less element
137
+ $unset: {reserved: ''}
138
+ };
139
+
140
+ if ((obj._env && obj._env.qcnt) > 1) {
141
+ debug ('it is certain there are still entries in the intraqueue: set mature and tries');
142
+ upd.$set = {
143
+ mature: obj._env.q[1].mature,
144
+ tries: obj._env.q[1].tries,
145
+ }
146
+ }
147
+ else {
148
+ // last in queue: set mature to distant future to get it out of the way while it's GCed
149
+ // not really, it'd impact if there were an insert in between
150
+ // upd.$set.mature = Queue.nowPlusSecs (60*60*24*1000);
151
+ }
152
+
153
+ const opts = {};
154
+
155
+ this._col.updateOne (q, upd, opts, (err, result) => {
156
+ debug ('commit: updateOne (%j, %j, %j) => (%j, %j)', q, upd, opts, err, result);
157
+ if (err) return callback (err);
158
+ callback (null, result && (result.modifiedCount == 1));
159
+ });
160
+ }
161
+
162
+
163
+ //////////////////////////////////
164
+ // rollback previous reserve, by p.id
165
+ rollback (id, next_t, callback) {
166
+ if (_.isFunction (next_t)) {
167
+ callback = next_t;
168
+ next_t = null;
169
+ }
170
+
171
+ const q = {
172
+ _id: id,
173
+ reserved: {$exists: true}
174
+ };
175
+
176
+ const upd = {
177
+ $set: {mature: (next_t ? new Date (next_t) : Queue.now ())},
178
+ $unset: {reserved: ''}
179
+ };
180
+
181
+ const opts = {};
182
+
183
+ this._col.updateOne (q, upd, opts, (err, result) => {
184
+ debug ('rollback: updateOne (%j, %j, %j) => (%j, %j)', q, upd, opts, err, result);
185
+ if (err) return callback (err);
186
+ callback (null, result && (result.modifiedCount == 1));
187
+ });
188
+ }
189
+
190
+
191
+ //////////////////////////////////
192
+ // queue size of non-mature elements only
193
+ totalSize (callback) {
194
+ const cursor = this._col.aggregate ([
195
+ {$match: {
196
+ qcnt: {$gt: 0}
197
+ }},
198
+ {$group:{_id:'t', v: {$sum: '$qcnt'}}}
199
+ ]);
200
+
201
+ cursor.toArray ((err, res) => {
202
+ debug ('calculating schedSize: aggregation pipeline returns %o', res);
203
+ if (err) return callback (err);
204
+ if (res.length == 0) return callback (null, 0);
205
+ callback (null, res[0].v);
206
+ });
207
+ }
208
+
209
+
210
+ //////////////////////////////////
211
+ // queue size NOT including non-mature elements
212
+ size (callback) {
213
+ const cursor = this._col.aggregate ([
214
+ {$match: {
215
+ mature: {$lte: Queue.now ()},
216
+ qcnt: {$gt: 0}
217
+ }},
218
+ {$group:{_id:'t', v: {$sum: '$qcnt'}}}
219
+ ]);
220
+
221
+ cursor.toArray ((err, res) => {
222
+ debug ('calculating schedSize: aggregation pipeline returns %o', res);
223
+ if (err) return callback (err);
224
+ if (res.length == 0) return callback (null, 0);
225
+ callback (null, res[0].v);
226
+ });
227
+ }
228
+
229
+
230
+ //////////////////////////////////
231
+ // queue size of non-mature elements only
232
+ schedSize (callback) {
233
+ const cursor = this._col.aggregate ([
234
+ {$match: {
235
+ mature: {$gt: Queue.now ()},
236
+ reserved: {$exists: false},
237
+ qcnt: {$gt: 0}
238
+ }},
239
+ {$group:{_id:'t', v: {$sum: '$qcnt'}}}
240
+ ]);
241
+
242
+ cursor.toArray ((err, res) => {
243
+ debug ('calculating schedSize: aggregation pipeline returns %o', res);
244
+ if (err) return callback (err);
245
+ if (res.length == 0) return callback (null, 0);
246
+ callback (null, res[0].v);
247
+ });
248
+ }
249
+
250
+
251
+ //////////////////////////////////
252
+ // queue size of reserved elements only
253
+ resvSize (callback) {
254
+ const cursor = this._col.aggregate ([
255
+ {$match: {
256
+ mature: {$gt: Queue.now ()},
257
+ reserved: {$exists: true},
258
+ qcnt: {$gt: 0}
259
+ }},
260
+ {$group:{_id:'t', v: {$sum: '$qcnt'}}}
261
+ ]);
262
+
263
+ cursor.toArray ((err, res) => {
264
+ debug ('calculating schedSize: aggregation pipeline returns %o', res);
265
+ if (err) return callback (err);
266
+ if (res.length == 0) return callback (null, 0);
267
+ callback (null, res[0].v);
268
+ });
269
+ }
270
+
271
+
272
+ //////////////////////////////////////////////
273
+ // remove by id
274
+ remove (id, callback) {
275
+ const q = {
276
+ _id: id,
277
+ qcnt: { $eq: 1 }, // allow deletion ONLY if it has just one element in the intraqueue
278
+ reserved: {$exists: false}
279
+ };
280
+
281
+ const opts = {};
282
+
283
+ this._col.deleteOne (q, opts, (err, result) => {
284
+ debug ('remove: deleteOne (%j, %j) => (%j, %j)', q, opts, err, result);
285
+ if (err) return callback (err);
286
+ callback (null, result && (result.deletedCount == 1));
287
+ });
288
+ }
289
+
290
+
291
+ /////////////////////////////////////////
292
+ // get element from queue
293
+ next_t (callback) {
294
+ this._col
295
+ .find ()
296
+ .limit(1)
297
+ .sort ({mature:1})
298
+ .project ({mature:1})
299
+ .next ((err, result) => {
300
+ if (err) return callback (err);
301
+ callback (null, result && result.mature);
302
+ });
303
+ }
304
+
305
+
306
+ ///////////////////////////////////////////////////////////////////////////////
307
+ // private parts
308
+
309
+ //////////////////////////////////////////////////////////////////
310
+ // create needed indexes for O(1) functioning
311
+ ensureIndexes (cb) {
312
+ async.series ([
313
+ cb => this._col.createIndex ({mature : 1, qcnt: 1}, cb),
314
+ ], cb);
315
+ }
316
+ }
317
+
318
+
319
+ class Factory extends QFactory_MongoDB_defaults {
320
+ constructor (opts, mongo_conn) {
321
+ super (opts);
322
+ this._mongo_conn = mongo_conn;
323
+ this._db = mongo_conn.db();
324
+ }
325
+
326
+ queue (name, opts) {
327
+ var full_opts = {};
328
+ _.merge(full_opts, this._opts, opts);
329
+ return new IntraOrderedQueue (name, this, full_opts, opts);
330
+ }
331
+
332
+ close (cb) {
333
+ super.close (() => {
334
+ if (this._mongo_conn) {
335
+ this._mongo_conn.close ();
336
+ this._mongo_conn = null;
337
+ }
338
+
339
+ if (cb) return cb ();
340
+ });
341
+ }
342
+
343
+ type () {
344
+ return IntraOrderedQueue.Type ();
345
+ }
346
+
347
+ capabilities () {
348
+ return {
349
+ sched: true,
350
+ reserve: true,
351
+ pipeline: false,
352
+ tape: false,
353
+ remove: false
354
+ };
355
+ }
356
+ }
357
+
358
+ function creator (opts, cb) {
359
+ var _opts = opts || {};
360
+ var m_url = _opts.url || 'mongodb://localhost:27017/keuss';
361
+
362
+ MongoClient.connect (m_url, { useNewUrlParser: true }, (err, cl) => {
363
+ if (err) return cb (err);
364
+ var F = new Factory (_opts, cl);
365
+ F.async_init (err => cb (null, F));
366
+ });
367
+ }
368
+
369
+ module.exports = creator;
370
+
371
+
372
+
373
+
374
+
@@ -0,0 +1,18 @@
1
+ version: '3'
2
+
3
+ services:
4
+ mongo:
5
+ image: mongo:4.4.2-bionic
6
+ command: --logpath /dev/null
7
+ restart: on-failure
8
+ ports:
9
+ - 27017:27017
10
+
11
+ redis:
12
+ image: redis:alpine
13
+ ports:
14
+ - 6379:6379
15
+
16
+ networks:
17
+ default:
18
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keuss",
3
- "version": "1.7.1",
3
+ "version": "1.7.2",
4
4
  "keywords": [
5
5
  "queue",
6
6
  "persistent",
@@ -27,23 +27,23 @@
27
27
  "license": "GPL-3.0",
28
28
  "dependencies": {
29
29
  "@nodebb/mubsub": "~1.8.0",
30
- "async": "~3.2.3",
30
+ "async": "~3.2.4",
31
31
  "async-lock": "~1.3.1",
32
32
  "debug": "~4.3.4",
33
33
  "ioredis": "~5.0.4",
34
34
  "lodash": "~4.17.21",
35
35
  "mitt": "~3.0.0",
36
36
  "mongodb": "~4.5.0",
37
- "uuid": "~8.3.2",
38
- "why-is-node-running": "^2.2.2"
37
+ "uuid": "~8.3.2"
39
38
  },
40
39
  "devDependencies": {
41
40
  "chance": "~1.1.8",
42
- "mocha": "~9.2.2",
43
- "should": "~13.2.3"
41
+ "mocha": "~10.2.0",
42
+ "should": "~13.2.3",
43
+ "why-is-node-running": "^2.2.2"
44
44
  },
45
45
  "scripts": {
46
- "test": "mocha --reporter spec --check-leaks --no-timeouts --exit test/",
47
- "test-with-coverage": "nyc --reporter=html -- mocha --reporter spec --check-leaks --no-timeouts --exit test/"
46
+ "test": "docker compose up -d; sleep 5; mocha --reporter spec --check-leaks --no-timeouts --exit test/ ; docker compose down",
47
+ "test-with-coverage": "docker compose up -d; sleep 5; nyc --reporter=html -- mocha --reporter spec --check-leaks --no-timeouts --exit ; test/docker compose down"
48
48
  }
49
49
  }
@@ -0,0 +1,53 @@
1
+ const async = require ('async');
2
+ const MQ = require ('../backends/intraorder');
3
+
4
+ // initialize factory
5
+ MQ ({
6
+ url: 'mongodb://localhost/keuss'
7
+ }, (err, factory) => {
8
+ if (err) return console.error(err);
9
+
10
+ const q = factory.queue ('test_queue', {});
11
+ /*
12
+ async.waterfall ([
13
+ cb => q.push ({iid: 123, elem: 1, headline: 'something something', tags: {a: 1, b: 2}}, cb),
14
+ (item_id, cb) => q.push ({iid: 123, elem: 2, headline: 'other other', tags: {a: 3, b: 4}}, cb),
15
+ (item_id, cb) => q.pop ('consumer-1', {reserve: true}, cb),
16
+ (item, cb) => q.ko (item, new Date().getTime () + 1500, cb),
17
+ (item_id, cb) => q.pop ('consumer-1', {reserve: true}, cb),
18
+ (item, cb) => {console.log ('%s: got %o', new Date().toISOString (), item.payload); q.ok (item, cb); },
19
+ (item_id, cb) => q.pop ('consumer-1', {reserve: true}, cb),
20
+ (item, cb) => q.ko (item, new Date().getTime () + 1500, cb),
21
+ (item_id, cb) => q.pop ('consumer-1', {reserve: true}, cb),
22
+ (item, cb) => {console.log ('%s: got %o', new Date().toISOString (), item.payload); q.ok (item, cb); },
23
+ (i, cb) => setTimeout (cb, 100),
24
+ cb => q.status (cb),
25
+ ], (err, res) => {
26
+ if (err) console.error (err);
27
+ console.log (res)
28
+ factory.close ();
29
+ });
30
+
31
+ */
32
+
33
+
34
+ async.series ([
35
+ cb => q.status (cb),
36
+ cb => q.push ({iid: 123, elem: 1, headline: 'something something', tags: {a: 1, b: 2}}, cb),
37
+ cb => q.status (cb),
38
+ cb => q.push ({iid: 123, elem: 2, headline: 'other other', tags: {a: 3, b: 4}}, cb),
39
+ cb => q.status (cb),
40
+ cb => q.pop ('consumer-1', cb),
41
+ cb => q.status (cb),
42
+ cb => q.pop ('consumer-1', cb),
43
+ cb => q.status (cb),
44
+ cb => setTimeout (cb, 100),
45
+ cb => q.status (cb),
46
+ ], (err, res) => {
47
+ if (err) console.error (err);
48
+ factory.close ();
49
+ res.forEach ((v, i) => console.log ('%d:', i, v ));
50
+ });
51
+
52
+ });
53
+
@@ -77,12 +77,13 @@ function release_mq_factory (q, factory, cb) {
77
77
 
78
78
  /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
79
79
  [
80
- {label: 'Simple MongoDB', mq: require ('../backends/mongo')},
81
- {label: 'Pipelined MongoDB', mq: require ('../backends/pl-mongo')},
82
- {label: 'Tape MongoDB', mq: require ('../backends/ps-mongo')},
83
- {label: 'Stream MongoDB', mq: require ('../backends/stream-mongo')},
84
- {label: 'Redis OrderedQueue', mq: require ('../backends/redis-oq')},
85
- {label: 'MongoDB SafeBucket', mq: require ('../backends/bucket-mongo-safe')}
80
+ // {label: 'Simple MongoDB', mq: require ('../backends/mongo')},
81
+ // {label: 'Pipelined MongoDB', mq: require ('../backends/pl-mongo')},
82
+ // {label: 'Tape MongoDB', mq: require ('../backends/ps-mongo')},
83
+ // {label: 'Stream MongoDB', mq: require ('../backends/stream-mongo')},
84
+ // {label: 'Redis OrderedQueue', mq: require ('../backends/redis-oq')},
85
+ // {label: 'MongoDB SafeBucket', mq: require ('../backends/bucket-mongo-safe')},
86
+ {label: 'Mongo IntraOrder', mq: require ('../backends/intraorder')},
86
87
  ].forEach(function (MQ_item) {
87
88
  describe('rollback and deadletters with ' + MQ_item.label + ' queue backend', function () {
88
89
  const MQ = MQ_item.mq;
@@ -10,13 +10,14 @@ var MongoClient = require ('mongodb').MongoClient;
10
10
  var factory = null;
11
11
 
12
12
  [
13
- {label: 'Simple MongoDB', mq: require ('../backends/mongo')},
14
- {label: 'Pipelined MongoDB', mq: require ('../backends/pl-mongo')},
15
- {label: 'Tape MongoDB', mq: require ('../backends/ps-mongo')},
16
- {label: 'Stream MongoDB', mq: require ('../backends/stream-mongo')},
17
- {label: 'Safe MongoDB Buckets', mq: require ('../backends/bucket-mongo-safe')},
18
- {label: 'Redis List', mq: require ('../backends/redis-list')},
19
- {label: 'Redis OrderedQueue', mq: require ('../backends/redis-oq')},
13
+ {label: 'Simple MongoDB', mq: require ('../backends/mongo')},
14
+ {label: 'Pipelined MongoDB', mq: require ('../backends/pl-mongo')},
15
+ {label: 'Tape MongoDB', mq: require ('../backends/ps-mongo')},
16
+ {label: 'Stream MongoDB', mq: require ('../backends/stream-mongo')},
17
+ {label: 'Safe MongoDB Buckets', mq: require ('../backends/bucket-mongo-safe')},
18
+ {label: 'Redis List', mq: require ('../backends/redis-list')},
19
+ {label: 'Redis OrderedQueue', mq: require ('../backends/redis-oq')},
20
+ {label: 'Mongo IntraOrder', mq: require ('../backends/intraorder')},
20
21
  ].forEach(function (MQ_item) {
21
22
  describe('payload aspects on ' + MQ_item.label + ' queue backend, round 1', function () {
22
23
  var MQ = MQ_item.mq;
@@ -15,7 +15,8 @@ var factory = null;
15
15
  {label: 'Tape MongoDB', mq: require ('../backends/ps-mongo')},
16
16
  {label: 'Stream MongoDB', mq: require ('../backends/stream-mongo')},
17
17
  // {label: 'Safe MongoDB Buckets', mq: require ('../backends/bucket-mongo-safe')},
18
- {label: 'Redis OrderedQueue', mq: require ('../backends/redis-oq')}
18
+ {label: 'Redis OrderedQueue', mq: require ('../backends/redis-oq')},
19
+ {label: 'Mongo IntraOrder', mq: require ('../backends/intraorder')},
19
20
  ].forEach(function (MQ_item) {
20
21
  describe('reserve-commit-rollback with ' + MQ_item.label + ' queue backend', function () {
21
22
  var MQ = MQ_item.mq;
@@ -130,7 +131,9 @@ var factory = null;
130
131
  cb();
131
132
  }),
132
133
  cb => q.next_t((err, res) => {
133
- should.equal(res, null);
134
+ if (q.type () != 'mongo:intraorder') {
135
+ should.equal(res, null);
136
+ }
134
137
  cb();
135
138
  })
136
139
  ], (err, results) => {
@@ -252,7 +255,9 @@ var factory = null;
252
255
  },
253
256
  function (cb) {
254
257
  q.next_t(function (err, res) {
255
- should.equal(res, null);
258
+ if (q.type () != 'mongo:intraorder') {
259
+ should.equal(res, null);
260
+ }
256
261
  cb();
257
262
  });
258
263
  }
@@ -604,6 +609,7 @@ var factory = null;
604
609
  it('should do raw reserve & commit as expected', function (done) {
605
610
  var q = factory.queue('test_queue_7');
606
611
  var id = null;
612
+ var obj = null;
607
613
 
608
614
  async.series([
609
615
  cb => q.push({elem: 1, pl: 'twetrwte'}, cb),
@@ -617,6 +623,7 @@ var factory = null;
617
623
  }),
618
624
  cb => q.reserve((err, res) => {
619
625
  id = res._id;
626
+ obj = res;
620
627
  res.payload.should.eql({
621
628
  elem: 1,
622
629
  pl: 'twetrwte'
@@ -639,7 +646,7 @@ var factory = null;
639
646
  cb => q.commit(id, (err, res) => {
640
647
  res.should.equal(true);
641
648
  cb();
642
- }),
649
+ }, obj),
643
650
  cb => q.size((err, size) => {
644
651
  size.should.equal(0);
645
652
  cb();
@@ -649,17 +656,20 @@ var factory = null;
649
656
  cb();
650
657
  }),
651
658
  cb => q.next_t((err, res) => {
652
- should.equal(res, null);
659
+ if (q.type () != 'mongo:intraorder') {
660
+ should.equal(res, null);
661
+ }
653
662
  cb();
654
663
  }),
655
664
  ], (err, results) => {
656
- done();
665
+ done(err);
657
666
  });
658
667
  });
659
668
 
660
669
  it('should do raw reserve & rollback as expected', function (done) {
661
670
  var q = factory.queue('test_queue_8');
662
671
  var id = null;
672
+ var obj = null;
663
673
 
664
674
  async.series([
665
675
  function (cb) {
@@ -683,6 +693,7 @@ var factory = null;
683
693
  function (cb) {
684
694
  q.reserve(function (err, res) {
685
695
  id = res._id;
696
+ obj = res;
686
697
  res.payload.should.eql({
687
698
  elem: 1,
688
699
  pl: 'twetrwte'
@@ -737,7 +748,7 @@ var factory = null;
737
748
  q.commit(id, function (err, res) {
738
749
  res.should.equal(false);
739
750
  cb();
740
- })
751
+ }, obj)
741
752
  },
742
753
  function (cb) {
743
754
  q.size(function (err, size) {
@@ -782,7 +793,9 @@ var factory = null;
782
793
  },
783
794
  function (cb) {
784
795
  q.next_t(function (err, res) {
785
- should.equal(res, null);
796
+ if (q.type () != 'mongo:intraorder') {
797
+ should.equal(res, null);
798
+ }
786
799
  cb();
787
800
  })
788
801
  },
@@ -794,6 +807,7 @@ var factory = null;
794
807
  it('should do get.reserve & ok as expected', function (done) {
795
808
  var q = factory.queue('test_queue_9');
796
809
  var id = null;
810
+ var obj = null;
797
811
 
798
812
  async.series([
799
813
  function (cb) {
@@ -807,6 +821,7 @@ var factory = null;
807
821
  reserve: true
808
822
  }, function (err, res) {
809
823
  id = res._id;
824
+ obj = res;
810
825
  res.payload.should.eql({
811
826
  elem: 1,
812
827
  pl: 'twetrwte'
@@ -834,7 +849,7 @@ var factory = null;
834
849
  })
835
850
  },
836
851
  function (cb) {
837
- q.ok(id, function (err, res) {
852
+ q.ok(obj, function (err, res) {
838
853
  res.should.equal(true);
839
854
  cb();
840
855
  })
@@ -853,7 +868,9 @@ var factory = null;
853
868
  },
854
869
  function (cb) {
855
870
  q.next_t(function (err, res) {
856
- should.equal(res, null);
871
+ if (q.type () != 'mongo:intraorder') {
872
+ should.equal(res, null);
873
+ }
857
874
  cb();
858
875
  })
859
876
  },
@@ -875,10 +892,11 @@ var factory = null;
875
892
  });
876
893
 
877
894
  it('should manage rollback on invalid id as expected', function (done) {
878
- if (MQ_item.label == 'Redis OrderedQueue') return done ();
879
-
880
895
  var q = factory.queue('test_queue_10');
881
896
 
897
+ if (q.type () == 'redis:oq') return done ();
898
+ if (q.type () == 'mongo:intraorder') return done ();
899
+
882
900
  async.series([
883
901
  function (cb) {
884
902
  q.rollback('invalid-id', function (err, res) {
@@ -924,10 +942,11 @@ var factory = null;
924
942
  cb => q.pop ('me', {reserve: true}, (err, res) => {
925
943
  if (err) return db (err);
926
944
  state.reserved_id = res._id;
945
+ state.reserved_obj = res;
927
946
  cb (null, res._id);
928
947
  }),
929
948
  cb => _get_all_sizes (q, cb),
930
- cb => q.ok (state.reserved_id, cb),
949
+ cb => q.ok (state.reserved_id, cb, state.reserved_obj),
931
950
  cb => _get_all_sizes (q, cb),
932
951
  cb => q.pop ('me', cb),
933
952
  cb => q.pop ('me', cb),
@@ -0,0 +1,325 @@
1
+ const async = require ('async');
2
+ const should = require ('should');
3
+ const _ = require ('lodash');
4
+
5
+ const LocalSignal = require ('../signal/local');
6
+ const MemStats = require ('../stats/mem');
7
+
8
+ const MongoClient = require ('mongodb').MongoClient;
9
+
10
+
11
+ const MQ = require ('../backends/intraorder');
12
+
13
+ var factory = null;
14
+
15
+
16
+ describe('IntraOrder backend: specific operations', () => {
17
+
18
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////
19
+ before(done => {
20
+ var opts = {
21
+ url: 'mongodb://localhost/keuss_test_intraorder',
22
+ signaller: {provider: LocalSignal},
23
+ stats: {provider: MemStats}
24
+ };
25
+
26
+ MQ(opts, (err, fct) => {
27
+ if (err) return done(err);
28
+ factory = fct;
29
+ done();
30
+ });
31
+ });
32
+
33
+
34
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////
35
+ after (done => async.series ([
36
+ cb => setTimeout (cb, 1000),
37
+ cb => factory.close (cb),
38
+ cb => MongoClient.connect ('mongodb://localhost/keuss_test_intraorder', (err, cl) => {
39
+ if (err) return done (err);
40
+ cl.db().dropDatabase (() => cl.close (cb))
41
+ })
42
+ ], done));
43
+
44
+
45
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////
46
+ it('sequential push & pops with no retries preserves order', done => {
47
+ const q = factory.queue('test_queue_1');
48
+
49
+ async.series([
50
+ cb => q.push({elem: 1, iid: 'twetrwte', pl: {d: 't-', a: 56}}, cb),
51
+ cb => q.push({elem: 2, iid: 'twetrwte', pl: {d: 't--', a: 156}}, cb),
52
+ cb => q.push({elem: 3, iid: 'twetrwte', pl: {d: 't---', a: 256}}, cb),
53
+ cb => q.push({elem: 4, iid: 'twetrwte', pl: {d: 't----', a: 356}}, cb),
54
+ cb => q.push({elem: 5, iid: 'twetrwte', pl: {d: 't-----', a: 456}}, cb),
55
+ cb => {
56
+ q.size ((err, size) => {
57
+ if (err) return done (err);
58
+ size.should.equal(5);
59
+ cb();
60
+ })
61
+ },
62
+ cb => q.stats((err, res) => {
63
+ if (err) return done (err);
64
+ res.should.match({
65
+ get: 0,
66
+ put: 5,
67
+ reserve: 0,
68
+ commit: 0,
69
+ rollback: 0,
70
+ deadletter: 0
71
+ });
72
+ cb();
73
+ }),
74
+ cb => q.pop('c1', cb),
75
+ cb => q.pop('c1', cb),
76
+ cb => q.pop('c1', cb),
77
+ cb => q.pop('c1', cb),
78
+ cb => q.pop('c1', cb),
79
+ cb => q.size((err, size) => {
80
+ if (err) return done (err);
81
+ size.should.equal(0);
82
+ cb();
83
+ }),
84
+ cb => q.stats((err, res) => {
85
+ if (err) return done (err);
86
+ res.should.match({
87
+ get: 5,
88
+ put: 5,
89
+ reserve: 0,
90
+ commit: 0,
91
+ rollback: 0,
92
+ deadletter: 0
93
+ });
94
+ cb();
95
+ }),
96
+ ], (err, results) => {
97
+ if (err) return done (err);
98
+ results[7].payload.should.eql ({ elem: 1, iid: 'twetrwte', pl: { d: 't-', a: 56 } });
99
+ results[8].payload.should.eql ({ elem: 2, iid: 'twetrwte', pl: { d: 't--', a: 156 } });
100
+ results[9].payload.should.eql ({ elem: 3, iid: 'twetrwte', pl: { d: 't---', a: 256 } });
101
+ results[10].payload.should.eql ({ elem: 4, iid: 'twetrwte', pl: { d: 't----', a: 356 } });
102
+ results[11].payload.should.eql ({ elem: 5, iid: 'twetrwte', pl: { d: 't-----', a: 456 } });
103
+ done();
104
+ });
105
+ });
106
+
107
+
108
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////
109
+ it('sequential reserve-commit with retries preserves order', done => {
110
+ const q = factory.queue('test_queue_2');
111
+ const t0 = process.hrtime();
112
+ async.series([
113
+ cb => q.push({elem: 1, iid: 'twetrwte', pl: {d: 't-', a: 56}}, cb),
114
+ cb => q.push({elem: 2, iid: 'twetrwte', pl: {d: 't--', a: 156}}, cb),
115
+ cb => q.push({elem: 3, iid: 'twetrwte', pl: {d: 't---', a: 256}}, cb),
116
+ cb => q.push({elem: 4, iid: 'twetrwte', pl: {d: 't----', a: 356}}, cb),
117
+ cb => q.push({elem: 5, iid: 'twetrwte', pl: {d: 't-----', a: 456}}, cb),
118
+
119
+ cb => q.pop('c1', {reserve: true}, (err, res) => {
120
+ if (err) return done (err);
121
+ res.payload.should.eql ({ elem: 1, iid: 'twetrwte', pl: { d: 't-', a: 56 } });
122
+ q.ko (res, new Date().getTime () + 2000, cb)
123
+ }),
124
+
125
+ cb => q.pop('c1', {reserve: true}, (err, res) => {
126
+ if (err) return done (err);
127
+ res.payload.should.eql ({ elem: 1, iid: 'twetrwte', pl: { d: 't-', a: 56 } });
128
+ q.ko (res, new Date().getTime () + 3000, cb)
129
+ }),
130
+
131
+ cb => q.pop('c1', {reserve: true}, (err, res) => {
132
+ if (err) return done (err);
133
+ res.payload.should.eql ({ elem: 1, iid: 'twetrwte', pl: { d: 't-', a: 56 } });
134
+ q.ko (res, new Date().getTime () + 1000, cb)
135
+ }),
136
+
137
+ // cb => q.pop('c1', (err, res) => {console.log (new Date().toISOString (), res.payload); cb (err, res)}),
138
+
139
+ cb => q.pop('c1', cb),
140
+ cb => q.pop('c1', cb),
141
+ cb => q.pop('c1', cb),
142
+ cb => q.pop('c1', cb),
143
+ cb => q.pop('c1', cb),
144
+
145
+ cb => q.size((err, size) => {
146
+ if (err) return done (err);
147
+ size.should.equal(0);
148
+ cb();
149
+ }),
150
+ ], (err, results) => {
151
+ if (err) return done (err);
152
+
153
+ // duration must be about 6 secs
154
+ const delta = process.hrtime(t0);
155
+ delta[0].should.eql (6);
156
+ results[8].payload.should.eql ({ elem: 1, iid: 'twetrwte', pl: { d: 't-', a: 56 } });
157
+ results[9].payload.should.eql ({ elem: 2, iid: 'twetrwte', pl: { d: 't--', a: 156 } });
158
+ results[10].payload.should.eql ({ elem: 3, iid: 'twetrwte', pl: { d: 't---', a: 256 } });
159
+ results[11].payload.should.eql ({ elem: 4, iid: 'twetrwte', pl: { d: 't----', a: 356 } });
160
+ results[12].payload.should.eql ({ elem: 5, iid: 'twetrwte', pl: { d: 't-----', a: 456 } });
161
+ done();
162
+ });
163
+ });
164
+
165
+
166
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////
167
+ it('parallel reserve-commit on same iid blocks until commit', done => {
168
+ const q = factory.queue('test_queue_3');
169
+ const t0 = process.hrtime();
170
+ const pops = [];
171
+
172
+ async.series([
173
+ cb => q.push({elem: 1, iid: 'twetrwte', pl: {d: 't-', a: 56}}, cb),
174
+ cb => q.push({elem: 2, iid: 'twetrwte', pl: {d: 't--', a: 156}}, cb),
175
+ cb => q.push({elem: 3, iid: 'twetrwte', pl: {d: 't---', a: 256}}, cb),
176
+
177
+ cb => async.parallel ([
178
+ cb => {
179
+ let elem;
180
+ async.series ([
181
+ cb => setTimeout (cb, 500),
182
+ cb => q.pop('c1', {reserve: true}, (err, res) => {elem = res; pops.push (res); cb (err); }),
183
+ cb => setTimeout (cb, 1500),
184
+ cb => q.ok (elem, cb),
185
+ ], cb);
186
+ },
187
+ cb => {
188
+ let elem;
189
+ async.series ([
190
+ cb => setTimeout (cb, 1000),
191
+ cb => q.pop('c1', {reserve: true}, (err, res) => {elem = res; pops.push (res);; cb (err); }),
192
+ cb => setTimeout (cb, 1500),
193
+ cb => q.ok (elem, cb),
194
+ ], cb);
195
+ },
196
+ cb => {
197
+ let elem;
198
+ async.series ([
199
+ cb => setTimeout (cb, 1500),
200
+ cb => q.pop('c1', {reserve: true}, (err, res) => {elem = res; pops.push (res);; cb (err); }),
201
+ cb => setTimeout (cb, 1500),
202
+ cb => q.ok (elem, cb),
203
+ ], cb);
204
+ },
205
+ ], cb)
206
+ ], (err, results) => {
207
+ if (err) return done (err);
208
+
209
+ pops[0].payload.should.eql ({ elem: 1, iid: 'twetrwte', pl: { d: 't-', a: 56 } });
210
+ pops[1].payload.should.eql ({ elem: 2, iid: 'twetrwte', pl: { d: 't--', a: 156 } });
211
+ pops[2].payload.should.eql ({ elem: 3, iid: 'twetrwte', pl: { d: 't---', a: 256 } });
212
+
213
+ // duration must be about 6 secs
214
+ const delta = process.hrtime(t0);
215
+
216
+ // queue pop won't rearm after commit... so it will timeout after a min and only then rearm again for next pop/reserve
217
+ // delta[0].should.eql (7);
218
+ done();
219
+ });
220
+ });
221
+
222
+
223
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////
224
+ it('parallel reserve-commit on same iid blocks until rollback', done => {
225
+ const q = factory.queue('test_queue_4');
226
+ const t0 = process.hrtime();
227
+ const pops = [];
228
+
229
+ async.series([
230
+ cb => q.push({elem: 1, iid: 'twetrwte', pl: {d: 't-', a: 56}}, cb),
231
+ cb => q.push({elem: 2, iid: 'twetrwte', pl: {d: 't--', a: 156}}, cb),
232
+ cb => q.push({elem: 3, iid: 'twetrwte', pl: {d: 't---', a: 256}}, cb),
233
+
234
+ cb => async.parallel ([
235
+ cb => {
236
+ let elem;
237
+ async.series ([
238
+ cb => setTimeout (cb, 500),
239
+ cb => q.pop('c1', {reserve: true}, (err, res) => {elem = res; pops.push (res); cb (err); }),
240
+ cb => q.ko (elem, new Date().getTime () + 1000, cb),
241
+ ], cb);
242
+ },
243
+ cb => {
244
+ let elem;
245
+ async.series ([
246
+ cb => setTimeout (cb, 700),
247
+ cb => q.pop('c1', {reserve: true}, (err, res) => {elem = res; pops.push (res);; cb (err); }),
248
+ cb => q.ko (elem, new Date().getTime () + 1000, cb),
249
+ ], cb);
250
+ },
251
+ cb => {
252
+ let elem;
253
+ async.series ([
254
+ cb => setTimeout (cb, 1500),
255
+ cb => q.pop('c1', {reserve: true}, (err, res) => {elem = res; pops.push (res);; cb (err); }),
256
+ cb => setTimeout (cb, 1500),
257
+ cb => q.ok (elem, cb),
258
+ ], cb);
259
+ },
260
+ cb => {
261
+ let elem;
262
+ async.series ([
263
+ cb => setTimeout (cb, 2000),
264
+ cb => q.pop('c1', {reserve: true}, (err, res) => {elem = res; pops.push (res);; cb (err); }),
265
+ cb => setTimeout (cb, 1500),
266
+ cb => q.ok (elem, cb),
267
+ ], cb);
268
+ },
269
+ cb => {
270
+ let elem;
271
+ async.series ([
272
+ cb => setTimeout (cb, 2500),
273
+ cb => q.pop('c1', {reserve: true}, (err, res) => {elem = res; pops.push (res);; cb (err); }),
274
+ cb => setTimeout (cb, 1500),
275
+ cb => q.ok (elem, cb),
276
+ ], cb);
277
+ },
278
+ ], cb)
279
+ ], (err, results) => {
280
+ if (err) return done (err);
281
+
282
+ pops[0].payload.should.eql ({ elem: 1, iid: 'twetrwte', pl: { d: 't-', a: 56 } });
283
+ pops[1].payload.should.eql ({ elem: 1, iid: 'twetrwte', pl: { d: 't-', a: 56 } });
284
+ pops[2].payload.should.eql ({ elem: 1, iid: 'twetrwte', pl: { d: 't-', a: 56 } });
285
+ pops[3].payload.should.eql ({ elem: 2, iid: 'twetrwte', pl: { d: 't--', a: 156 } });
286
+ pops[4].payload.should.eql ({ elem: 3, iid: 'twetrwte', pl: { d: 't---', a: 256 } });
287
+
288
+ // duration must be about 6 secs
289
+ const delta = process.hrtime(t0);
290
+
291
+ // queue pop won't rearm after commit... so it will timeout after a min and only then rearm again for next pop/reserve
292
+ // delta[0].should.eql (7);
293
+ done();
294
+ });
295
+ });
296
+
297
+
298
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////
299
+ it('delayed push preserves order', done => {
300
+ const q = factory.queue('test_queue_1');
301
+
302
+ async.series([
303
+ cb => q.push({elem: 1, iid: 'twetrwte', pl: {d: 't-', a: 56}}, {delay: 5}, cb),
304
+ cb => q.push({elem: 2, iid: 'twetrwte', pl: {d: 't--', a: 156}}, {delay: 4}, cb),
305
+ cb => q.push({elem: 3, iid: 'twetrwte', pl: {d: 't---', a: 256}}, {delay: 3}, cb),
306
+ cb => q.push({elem: 4, iid: 'twetrwte', pl: {d: 't----', a: 356}}, {delay: 2}, cb),
307
+ cb => q.push({elem: 5, iid: 'twetrwte', pl: {d: 't-----', a: 456}}, {delay: 1}, cb),
308
+ cb => q.pop('c1', cb),
309
+ cb => q.pop('c1', cb),
310
+ cb => q.pop('c1', cb),
311
+ cb => q.pop('c1', cb),
312
+ cb => q.pop('c1', cb),
313
+ ], (err, results) => {
314
+ if (err) return done (err);
315
+ results[5].payload.should.eql ({ elem: 1, iid: 'twetrwte', pl: { d: 't-', a: 56 } });
316
+ results[6].payload.should.eql ({ elem: 2, iid: 'twetrwte', pl: { d: 't--', a: 156 } });
317
+ results[7].payload.should.eql ({ elem: 3, iid: 'twetrwte', pl: { d: 't---', a: 256 } });
318
+ results[8].payload.should.eql ({ elem: 4, iid: 'twetrwte', pl: { d: 't----', a: 356 } });
319
+ results[9].payload.should.eql ({ elem: 5, iid: 'twetrwte', pl: { d: 't-----', a: 456 } });
320
+ done();
321
+ });
322
+ });
323
+
324
+
325
+ });
package/test/pause.js CHANGED
@@ -14,6 +14,7 @@ var factory = null;
14
14
  {label: 'Stream MongoDB', backend: require ('../backends/stream-mongo')},
15
15
  {label: 'plain MongoDB', backend: require ('../backends/mongo')},
16
16
  {label: 'Safe MongoDB Bucket', backend: require ('../backends/bucket-mongo-safe')},
17
+ {label: 'Mongo IntraOrder', backend: require ('../backends/intraorder')},
17
18
  ].forEach (backend_item => {
18
19
  [
19
20
  {label: 'mem', stats: require('../stats/mem')},