concurrentCallerTest.js (10909B)
1 "use strict"; 2 3 describe("ConcurrentCaller", function () { 4 Components.utils.import("resource://zotero/concurrentCaller.js"); 5 var logger = null; 6 // Uncomment to get debug output 7 //logger = Zotero.debug; 8 9 describe("#start()", function () { 10 it("should run functions as slots open and wait for them to complete", function* () { 11 var numConcurrent = 2; 12 var running = 0; 13 var finished = 0; 14 var failed = false; 15 16 var ids = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']; 17 var funcs = ids.map(function (id) { 18 return Zotero.Promise.coroutine(function* () { 19 if (logger) { 20 Zotero.debug("Running " + id); 21 } 22 running++; 23 if (running > numConcurrent) { 24 failed = true; 25 throw new Error("Too many concurrent tasks"); 26 } 27 var min = 10; 28 var max = 25; 29 yield Zotero.Promise.delay( 30 Math.floor(Math.random() * (max - min + 1)) + min 31 ); 32 if (running > numConcurrent) { 33 failed = true; 34 throw new Error("Too many concurrent tasks"); 35 } 36 running--; 37 finished++; 38 if (logger) { 39 Zotero.debug("Finished " + id); 40 } 41 return id; 42 }); 43 }) 44 45 var caller = new ConcurrentCaller({ 46 numConcurrent, 47 logger 48 }); 49 var results = yield caller.start(funcs); 50 51 assert.equal(results.length, ids.length); 52 assert.equal(running, 0); 53 assert.equal(finished, ids.length); 54 assert.isFalse(failed); 55 }) 56 57 it("should add functions to existing queue and resolve when all are complete (waiting for earlier set)", function* () { 58 var numConcurrent = 2; 59 var running = 0; 60 var finished = 0; 61 var failed = false; 62 63 var ids1 = {"1": 5, "2": 10, "3": 7}; 64 var ids2 = {"4": 50, "5": 50}; 65 var makeFunc = function (id, delay) { 66 return Zotero.Promise.coroutine(function* () { 67 if (logger) { 68 Zotero.debug("Running " + id); 69 } 70 running++; 71 if (running > numConcurrent) { 72 failed = true; 73 throw new Error("Too many concurrent tasks"); 74 } 75 yield Zotero.Promise.delay(delay); 76 if (running > numConcurrent) { 77 failed = true; 78 throw new Error("Too many concurrent tasks"); 79 } 80 running--; 81 finished++; 82 if (logger) { 83 Zotero.debug("Finished " + id); 84 } 85 return id; 86 }); 87 }; 88 var keys1 = Object.keys(ids1); 89 var keys2 = Object.keys(ids2); 90 var funcs1 = Object.keys(ids1).map(id => makeFunc(id, ids1[id])); 91 var funcs2 = Object.keys(ids2).map(id => makeFunc(id, ids2[id])); 92 93 var caller = new ConcurrentCaller({ 94 numConcurrent, 95 logger 96 }); 97 var promise1 = caller.start(funcs1); 98 yield Zotero.Promise.delay(1); 99 var promise2 = caller.start(funcs2); 100 101 // Wait for first set 102 var results1 = yield promise1; 103 104 // Second set shouldn't be done yet 105 assert.isFalse(promise2.isFulfilled()); 106 assert.equal(finished, keys1.length); 107 assert.equal(results1.length, keys1.length); 108 assert.sameMembers(results1.map(p => p.value()), keys1); 109 assert.isFalse(failed); 110 }) 111 112 it("should add functions to existing queue and resolve when all are complete (waiting for later set)", function* () { 113 var numConcurrent = 2; 114 var running = 0; 115 var finished = 0; 116 var failed = false; 117 118 var ids1 = {"1": 100, "2": 45, "3": 80}; 119 var ids2 = {"4": 1, "5": 1}; 120 var makeFunc = function (id, delay) { 121 return Zotero.Promise.coroutine(function* () { 122 if (logger) { 123 Zotero.debug("Running " + id); 124 } 125 running++; 126 if (running > numConcurrent) { 127 failed = true; 128 throw new Error("Too many concurrent tasks"); 129 } 130 yield Zotero.Promise.delay(delay); 131 if (running > numConcurrent) { 132 failed = true; 133 throw new Error("Too many concurrent tasks"); 134 } 135 running--; 136 finished++; 137 if (logger) { 138 Zotero.debug("Finished " + id); 139 } 140 return id; 141 }); 142 }; 143 var keys1 = Object.keys(ids1); 144 var keys2 = Object.keys(ids2); 145 var funcs1 = Object.keys(ids1).map(id => makeFunc(id, ids1[id])); 146 var funcs2 = Object.keys(ids2).map(id => makeFunc(id, ids2[id])); 147 148 var caller = new ConcurrentCaller({ 149 numConcurrent, 150 logger 151 }); 152 var promise1 = caller.start(funcs1); 153 yield Zotero.Promise.delay(10); 154 var promise2 = caller.start(funcs2); 155 156 // Wait for second set 157 var results2 = yield promise2; 158 159 // The second set should finish before the first 160 assert.isFalse(promise1.isFulfilled()); 161 assert.equal(running, 1); // 3 should still be running 162 assert.equal(finished, 4); // 1, 2, 4, 5 163 assert.equal(results2.length, keys2.length); 164 assert.equal(results2[0].value(), keys2[0]); 165 assert.equal(results2[1].value(), keys2[1]); 166 assert.isFalse(failed); 167 }) 168 169 it("should return a rejected promise if a single passed function fails", function* () { 170 var numConcurrent = 2; 171 172 var caller = new ConcurrentCaller({ 173 numConcurrent, 174 logger 175 }); 176 var e = yield getPromiseError(caller.start(function () { 177 throw new Error("Fail"); 178 })); 179 assert.ok(e); 180 }) 181 182 it("should stop on error if stopOnError is set", function* () { 183 var numConcurrent = 2; 184 var running = 0; 185 var finished = 0; 186 var failed = false; 187 188 var ids1 = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm']; 189 var ids2 = ['n', 'o', 'p', 'q']; 190 var makeFunc = function (id) { 191 return Zotero.Promise.coroutine(function* () { 192 if (logger) { 193 Zotero.debug("Running " + id); 194 } 195 running++ 196 if (running > numConcurrent) { 197 failed = true; 198 throw new Error("Too many concurrent tasks"); 199 } 200 var min = 10; 201 var max = 25; 202 yield Zotero.Promise.delay( 203 Math.floor(Math.random() * (max - min + 1)) + min 204 ); 205 if (id == 'g') { 206 running--; 207 finished++; 208 Zotero.debug("Throwing " + id); 209 // This causes an erroneous "possibly unhandled rejection" message in 210 // Bluebird 2.10.2 that I can't seem to get rid of (and the rejection 211 // is later handled), so tell Bluebird to ignore it 212 let e = new Error("Fail"); 213 e.handledRejection = true; 214 throw e; 215 } 216 if (running > numConcurrent) { 217 failed = true; 218 throw new Error("Too many concurrent tasks"); 219 } 220 running--; 221 finished++; 222 if (logger) { 223 Zotero.debug("Finished " + id); 224 } 225 return id; 226 }); 227 }; 228 var funcs1 = ids1.map(makeFunc) 229 var funcs2 = ids2.map(makeFunc) 230 231 var caller = new ConcurrentCaller({ 232 numConcurrent, 233 stopOnError: true, 234 logger 235 }); 236 var promise1 = caller.start(funcs1); 237 var promise2 = caller.start(funcs2); 238 239 var results1 = yield promise1; 240 241 assert.isTrue(promise2.isFulfilled()); 242 assert.equal(running, 0); 243 assert.isBelow(finished, ids1.length); 244 assert.equal(results1.length, ids1.length); 245 assert.equal(promise2.value().length, ids2.length); 246 // 'a' should be fulfilled 247 assert.isTrue(results1[0].isFulfilled()); 248 // 'g' should be rejected 249 assert.isTrue(results1[6].isRejected()); 250 // 'm' should be rejected 251 assert.isTrue(results1[12].isRejected()); 252 // All promises in second batch should be rejected 253 assert.isTrue(promise2.value().every(p => p.isRejected())); 254 }) 255 256 257 it("should not stop on error if stopOnError isn't set", function* () { 258 var numConcurrent = 2; 259 var running = 0; 260 var finished = 0; 261 var failed = false; 262 263 var ids1 = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm']; 264 var ids2 = ['n', 'o', 'p', 'q']; 265 var makeFunc = function (id) { 266 return Zotero.Promise.coroutine(function* () { 267 if (logger) { 268 Zotero.debug("Running " + id); 269 } 270 running++ 271 if (running > numConcurrent) { 272 failed = true; 273 throw new Error("Too many concurrent tasks"); 274 } 275 var min = 10; 276 var max = 25; 277 yield Zotero.Promise.delay( 278 Math.floor(Math.random() * (max - min + 1)) + min 279 ); 280 if (id == 'g') { 281 running--; 282 finished++; 283 Zotero.debug("Throwing " + id); 284 // This causes an erroneous "possibly unhandled rejection" message in 285 // Bluebird 2.10.2 that I can't seem to get rid of (and the rejection 286 // is later handled), so tell Bluebird to ignore it 287 let e = new Error("Fail"); 288 e.handledRejection = true; 289 throw e; 290 } 291 if (running > numConcurrent) { 292 failed = true; 293 throw new Error("Too many concurrent tasks"); 294 } 295 running--; 296 finished++; 297 if (logger) { 298 Zotero.debug("Finished " + id); 299 } 300 return id; 301 }); 302 }; 303 var funcs1 = ids1.map(makeFunc) 304 var funcs2 = ids2.map(makeFunc) 305 306 var caller = new ConcurrentCaller({ 307 numConcurrent, 308 logger 309 }); 310 var promise1 = caller.start(funcs1); 311 var promise2 = caller.start(funcs2); 312 313 var results2 = yield promise2; 314 315 assert.isTrue(promise1.isFulfilled()); 316 assert.isTrue(promise2.isFulfilled()); 317 assert.equal(running, 0); 318 assert.equal(finished, ids1.length + ids2.length); 319 assert.equal(promise1.value().length, ids1.length); 320 assert.equal(results2.length, ids2.length); 321 // 'a' should be fulfilled 322 assert.isTrue(promise1.value()[0].isFulfilled()); 323 // 'g' should be rejected 324 assert.isTrue(promise1.value()[6].isRejected()); 325 // 'm' should be fulfilled 326 assert.isTrue(promise1.value()[12].isFulfilled()); 327 // All promises in second batch should be fulfilled 328 assert.isTrue(results2.every(p => p.isFulfilled())); 329 }) 330 }) 331 332 describe("#wait()", function () { 333 it("should return when all tasks are done", function* () { 334 var numConcurrent = 2; 335 var running = 0; 336 var finished = 0; 337 var failed = false; 338 339 var ids1 = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm']; 340 var ids2 = ['n', 'o', 'p', 'q']; 341 var makeFunc = function (id) { 342 return Zotero.Promise.coroutine(function* () { 343 if (logger) { 344 Zotero.debug("Running " + id); 345 } 346 running++; 347 if (running > numConcurrent) { 348 failed = true; 349 throw new Error("Too many concurrent tasks"); 350 } 351 var min = 10; 352 var max = 25; 353 yield Zotero.Promise.delay( 354 Math.floor(Math.random() * (max - min + 1)) + min 355 ); 356 if (running > numConcurrent) { 357 failed = true; 358 throw new Error("Too many concurrent tasks"); 359 } 360 running--; 361 finished++; 362 if (logger) { 363 Zotero.debug("Finished " + id); 364 } 365 return id; 366 }); 367 }; 368 var funcs1 = ids1.map(makeFunc) 369 var funcs2 = ids2.map(makeFunc) 370 371 var caller = new ConcurrentCaller({ 372 numConcurrent, 373 logger 374 }); 375 var promise1 = caller.start(funcs1); 376 yield Zotero.Promise.delay(10); 377 var promise2 = caller.start(funcs2); 378 379 yield caller.wait(); 380 381 assert.isTrue(promise1.isFulfilled()); 382 assert.isTrue(promise2.isFulfilled()); 383 assert.equal(running, 0); 384 assert.equal(finished, ids1.length + ids2.length); 385 }) 386 }) 387 })