promise.test.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  1. var chai = require('chai'),
  2. expect = chai.expect,
  3. moment = require('moment'),
  4. sinon = require('sinon');
  5. var delay = ms => new Promise(_ => setTimeout(_, ms));
  6. chai.use(require('chai-as-promised'));
  7. sinon.usingPromise(Promise);
  8. describe('Global Promise', function() {
  9. var retry = require('../').default;
  10. var applyJitter = require('../').applyJitter;
  11. beforeEach(function() {
  12. this.count = 0;
  13. this.soRejected = new Error(Math.random().toString());
  14. this.soResolved = new Error(Math.random().toString());
  15. });
  16. it('should reject immediately if max is 1 (using options)', function() {
  17. var callback = sinon.stub();
  18. callback.resolves(this.soResolved);
  19. callback.onCall(0).rejects(this.soRejected);
  20. return expect(retry(callback, { max: 1, backoffBase: 0 }))
  21. .to.eventually.be.rejectedWith(this.soRejected)
  22. .then(function() {
  23. expect(callback.callCount).to.equal(1);
  24. });
  25. });
  26. it('should reject immediately if max is 1 (using integer)', function() {
  27. var callback = sinon.stub();
  28. callback.resolves(this.soResolved);
  29. callback.onCall(0).rejects(this.soRejected);
  30. return expect(retry(callback, 1))
  31. .to.eventually.be.rejectedWith(this.soRejected)
  32. .then(function() {
  33. expect(callback.callCount).to.equal(1);
  34. });
  35. });
  36. it('should reject after all tries if still rejected', function() {
  37. var callback = sinon.stub();
  38. callback.rejects(this.soRejected);
  39. return expect(retry(callback, { max: 3, backoffBase: 0 }))
  40. .to.eventually.be.rejectedWith(this.soRejected)
  41. .then(function() {
  42. expect(callback.firstCall.args).to.deep.equal([{ current: 1 }]);
  43. expect(callback.secondCall.args).to.deep.equal([{ current: 2 }]);
  44. expect(callback.thirdCall.args).to.deep.equal([{ current: 3 }]);
  45. expect(callback.callCount).to.equal(3);
  46. });
  47. });
  48. it('should resolve immediately if resolved on first try', function() {
  49. var callback = sinon.stub();
  50. callback.resolves(this.soResolved);
  51. callback.onCall(0).resolves(this.soResolved);
  52. return expect(retry(callback, { max: 10, backoffBase: 0 }))
  53. .to.eventually.equal(this.soResolved)
  54. .then(function() {
  55. expect(callback.callCount).to.equal(1);
  56. });
  57. });
  58. it('should resolve if resolved before hitting max', function() {
  59. var callback = sinon.stub();
  60. callback.rejects(this.soRejected);
  61. callback.onCall(3).resolves(this.soResolved);
  62. return expect(retry(callback, { max: 10, backoffBase: 0 }))
  63. .to.eventually.equal(this.soResolved)
  64. .then(function() {
  65. expect(callback.firstCall.args).to.deep.equal([{ current: 1 }]);
  66. expect(callback.secondCall.args).to.deep.equal([{ current: 2 }]);
  67. expect(callback.thirdCall.args).to.deep.equal([{ current: 3 }]);
  68. expect(callback.callCount).to.equal(4);
  69. });
  70. });
  71. describe('options.timeout', function() {
  72. it('should throw if reject on first attempt', function() {
  73. return expect(
  74. retry(
  75. function() {
  76. return delay(2000);
  77. },
  78. {
  79. max: 1,
  80. backoffBase: 0,
  81. timeout: 1000
  82. }
  83. )
  84. ).to.eventually.be.rejectedWith(retry.TimeoutError);
  85. });
  86. it('should throw if reject on last attempt', function() {
  87. return expect(
  88. retry(
  89. function() {
  90. this.count++;
  91. if (this.count === 3) {
  92. return delay(3500);
  93. }
  94. return Promise.reject();
  95. }.bind(this),
  96. {
  97. max: 3,
  98. backoffBase: 0,
  99. timeout: 1500
  100. }
  101. )
  102. )
  103. .to.eventually.be.rejectedWith(retry.TimeoutError)
  104. .then(function() {
  105. expect(this.count).to.equal(3);
  106. }.bind(this));
  107. });
  108. });
  109. describe('options.match', function() {
  110. it('should continue retry while error is equal to match string', function() {
  111. var callback = sinon.stub();
  112. callback.rejects(this.soRejected);
  113. callback.onCall(3).resolves(this.soResolved);
  114. return expect(
  115. retry(callback, {
  116. max: 15,
  117. backoffBase: 0,
  118. match: 'Error: ' + this.soRejected.message
  119. })
  120. )
  121. .to.eventually.equal(this.soResolved)
  122. .then(function() {
  123. expect(callback.callCount).to.equal(4);
  124. });
  125. });
  126. it('should reject immediately if error is not equal to match string', function() {
  127. var callback = sinon.stub();
  128. callback.rejects(this.soRejected);
  129. return expect(
  130. retry(callback, {
  131. max: 15,
  132. backoffBase: 0,
  133. match: 'A custom error string'
  134. })
  135. )
  136. .to.eventually.be.rejectedWith(this.soRejected)
  137. .then(function() {
  138. expect(callback.callCount).to.equal(1);
  139. });
  140. });
  141. it('should continue retry while error is instanceof match', function() {
  142. var callback = sinon.stub();
  143. callback.rejects(this.soRejected);
  144. callback.onCall(4).resolves(this.soResolved);
  145. return expect(retry(callback, { max: 15, backoffBase: 0, match: Error }))
  146. .to.eventually.equal(this.soResolved)
  147. .then(function() {
  148. expect(callback.callCount).to.equal(5);
  149. });
  150. });
  151. it('should reject immediately if error is not instanceof match', function() {
  152. var callback = sinon.stub();
  153. callback.rejects(this.soRejected);
  154. return expect(
  155. retry(callback, { max: 15, backoffBase: 0, match: function foo() {} })
  156. )
  157. .to.eventually.be.rejectedWith(Error)
  158. .then(function() {
  159. expect(callback.callCount).to.equal(1);
  160. });
  161. });
  162. it('should continue retry while error is equal to match string in array', function() {
  163. var callback = sinon.stub();
  164. callback.rejects(this.soRejected);
  165. callback.onCall(4).resolves(this.soResolved);
  166. return expect(
  167. retry(callback, {
  168. max: 15,
  169. backoffBase: 0,
  170. match: [
  171. 'Error: ' + (this.soRejected.message + 1),
  172. 'Error: ' + this.soRejected.message
  173. ]
  174. })
  175. )
  176. .to.eventually.equal(this.soResolved)
  177. .then(function() {
  178. expect(callback.callCount).to.equal(5);
  179. });
  180. });
  181. it('should reject immediately if error is not equal to match string in array', function() {
  182. var callback = sinon.stub();
  183. callback.rejects(this.soRejected);
  184. return expect(
  185. retry(callback, {
  186. max: 15,
  187. backoffBase: 0,
  188. match: [
  189. 'Error: ' + (this.soRejected + 1),
  190. 'Error: ' + (this.soRejected + 2)
  191. ]
  192. })
  193. )
  194. .to.eventually.be.rejectedWith(Error)
  195. .then(function() {
  196. expect(callback.callCount).to.equal(1);
  197. });
  198. });
  199. it('should reject immediately if error is not instanceof match in array', function() {
  200. var callback = sinon.stub();
  201. callback.rejects(this.soRejected);
  202. return expect(
  203. retry(callback, {
  204. max: 15,
  205. backoffBase: 0,
  206. match: ['Error: ' + (this.soRejected + 1), function foo() {}]
  207. })
  208. )
  209. .to.eventually.be.rejectedWith(Error)
  210. .then(function() {
  211. expect(callback.callCount).to.equal(1);
  212. });
  213. });
  214. it('should continue retry while error is instanceof match in array', function() {
  215. var callback = sinon.stub();
  216. callback.rejects(this.soRejected);
  217. callback.onCall(4).resolves(this.soResolved);
  218. return expect(
  219. retry(callback, {
  220. max: 15,
  221. backoffBase: 0,
  222. match: ['Error: ' + (this.soRejected + 1), Error]
  223. })
  224. )
  225. .to.eventually.equal(this.soResolved)
  226. .then(function() {
  227. expect(callback.callCount).to.equal(5);
  228. });
  229. });
  230. it('should continue retry while error is matched by function', function() {
  231. var callback = sinon.stub();
  232. callback.rejects(this.soRejected);
  233. callback.onCall(4).resolves(this.soResolved);
  234. return expect(
  235. retry(callback, {
  236. max: 15,
  237. backoffBase: 0,
  238. match: (err) => err instanceof Error
  239. })
  240. )
  241. .to.eventually.equal(this.soResolved)
  242. .then(function() {
  243. expect(callback.callCount).to.equal(5);
  244. });
  245. });
  246. it('should continue retry while error is matched by a function in array', function() {
  247. var callback = sinon.stub();
  248. callback.rejects(this.soRejected);
  249. callback.onCall(4).resolves(this.soResolved);
  250. return expect(
  251. retry(callback, {
  252. max: 15,
  253. backoffBase: 0,
  254. match: [
  255. (err) => err instanceof Error
  256. ]
  257. })
  258. )
  259. .to.eventually.equal(this.soResolved)
  260. .then(function() {
  261. expect(callback.callCount).to.equal(5);
  262. });
  263. });
  264. });
  265. describe('options.backoff', function() {
  266. it('should resolve after 5 retries and an eventual delay over 611ms using default backoff', async function() {
  267. // Given
  268. var callback = sinon.stub();
  269. callback.rejects(this.soRejected);
  270. callback.onCall(5).resolves(this.soResolved);
  271. // When
  272. var startTime = moment();
  273. const result = await retry(callback, { max: 15 });
  274. var endTime = moment();
  275. // Then
  276. expect(result).to.equal(this.soResolved);
  277. expect(callback.callCount).to.equal(6);
  278. expect(endTime.diff(startTime)).to.be.within(600, 650);
  279. });
  280. it('should resolve after 1 retry and initial delay equal to the backoffBase', async function() {
  281. var initialDelay = 100;
  282. var callback = sinon.stub();
  283. callback.onCall(0).rejects(this.soRejected);
  284. callback.onCall(1).resolves(this.soResolved);
  285. var startTime = moment();
  286. const result = await retry(callback, {
  287. max: 2,
  288. backoffBase: initialDelay,
  289. backoffExponent: 3
  290. });
  291. var endTime = moment();
  292. expect(result).to.equal(this.soResolved);
  293. expect(callback.callCount).to.equal(2);
  294. // allow for some overhead
  295. expect(endTime.diff(startTime)).to.be.within(initialDelay, initialDelay + 50);
  296. });
  297. it('should throw TimeoutError and cancel backoff delay if timeout is reached', function() {
  298. return expect(
  299. retry(
  300. function() {
  301. return delay(2000);
  302. },
  303. {
  304. max: 15,
  305. timeout: 1000
  306. }
  307. )
  308. ).to.eventually.be.rejectedWith(retry.TimeoutError);
  309. });
  310. });
  311. describe('options.report', function() {
  312. it('should receive the error that triggered a retry', function() {
  313. var report = sinon.stub();
  314. var callback = sinon.stub();
  315. callback.rejects(this.soRejected);
  316. callback.onCall(1).resolves(this.soResolved);
  317. return expect(
  318. retry(callback, {max: 3, report})
  319. )
  320. .to.eventually.equal(this.soResolved)
  321. .then(function() {
  322. expect(callback.callCount).to.equal(2);
  323. // messages sent to report are:
  324. // Trying functionStub #1 at <timestamp>
  325. // Error: <random number> <--- This is the report call we want to test
  326. // Retrying functionStub (2)
  327. // Delaying retry of functionStub by 100
  328. // Trying functionStub #2 at <timestamp>
  329. expect(report.callCount).to.equal(5);
  330. expect(report.getCall(1).args[2]).to.be.instanceOf(Error);
  331. });
  332. });
  333. it('should receive the error that exceeded max', function() {
  334. var report = sinon.stub();
  335. var callback = sinon.stub();
  336. callback.rejects(this.soRejected);
  337. return expect(
  338. retry(callback, {max: 3, report})
  339. )
  340. .to.eventually.be.rejectedWith(Error)
  341. .then(function() {
  342. expect(callback.callCount).to.equal(3);
  343. // Trying functionStub #1 at <timestamp>
  344. // Error: <random number>
  345. // Retrying functionStub (2)
  346. // Delaying retry of functionStub by 100
  347. // Trying functionStub #2 at <timestamp>
  348. // Error: <random number>
  349. // Retrying functionStub (3)
  350. // Delaying retry of functionStub by 110.00000000000001
  351. // Trying functionStub #3 at <timestamp>
  352. // Error: <random number> <--- This is the report call we want to test
  353. expect(report.callCount).to.equal(10);
  354. expect(report.lastCall.args[2]).to.be.instanceOf(Error);
  355. });
  356. });
  357. });
  358. describe('options.backoffJitter', function() {
  359. describe('fn:applyJitter', function() {
  360. it('applies randomized offsets to base delay', function() {
  361. for (let i = 0; i < 10; i++) {
  362. const withJitter = applyJitter(1000, 100);
  363. expect((withJitter >= 900 && withJitter <= 1100)).to.equal(true);
  364. }
  365. });
  366. it('never returns values less than zero', function() {
  367. for (let i = 0; i < 10; i++) {
  368. expect(applyJitter(10, 1000) >= 0).to.equal(true);
  369. }
  370. });
  371. });
  372. it('should resolve after 1 retries and an eventual delay in range of 80-120 ms', async function() {
  373. var initialDelay = 100;
  374. var delayJitter = 20;
  375. // Given
  376. var callback = sinon.stub();
  377. callback.rejects(this.soRejected);
  378. callback.onCall(1).resolves(this.soResolved);
  379. // When
  380. var startTime = moment();
  381. const result = await retry(callback, { max: 5, backoffBase: initialDelay, backoffJitter: delayJitter });
  382. var endTime = moment();
  383. // Then
  384. expect(result).to.equal(this.soResolved);
  385. expect(callback.callCount).to.equal(2);
  386. expect(endTime.diff(startTime)).to.be.within(75, 125);
  387. });
  388. });
  389. });