@@ -80,13 +80,13 @@ describe('TransactionExecutor', () => {
8080
8181 it ( 'should stop retrying when time expires' , done => {
8282 const executor = new TransactionExecutor ( ) ;
83- let workInvocationCounter = 0 ;
83+ const usedTransactions = [ ] ;
8484 const realWork = transactionWork ( [ SERVICE_UNAVAILABLE , SESSION_EXPIRED , TRANSIENT_ERROR_1 , TRANSIENT_ERROR_2 ] , 42 ) ;
8585
8686 const result = executor . execute ( transactionCreator ( ) , tx => {
8787 expect ( tx ) . toBeDefined ( ) ;
88- workInvocationCounter ++ ;
89- if ( workInvocationCounter === 3 ) {
88+ usedTransactions . push ( tx ) ;
89+ if ( usedTransactions . length === 3 ) {
9090 const currentTime = Date . now ( ) ;
9191 clock = lolex . install ( ) ;
9292 clock . setSystemTime ( currentTime + 30001 ) ; // move `Date.now()` call forward by 30 seconds
@@ -95,7 +95,8 @@ describe('TransactionExecutor', () => {
9595 } ) ;
9696
9797 result . catch ( error => {
98- expect ( workInvocationCounter ) . toEqual ( 3 ) ;
98+ expect ( usedTransactions . length ) . toEqual ( 3 ) ;
99+ expectAllTransactionsToBeClosed ( usedTransactions ) ;
99100 expect ( error . code ) . toEqual ( TRANSIENT_ERROR_1 ) ;
100101 done ( ) ;
101102 } ) ;
@@ -152,6 +153,14 @@ describe('TransactionExecutor', () => {
152153 ) ;
153154 } ) ;
154155
156+ it ( 'should retry when transaction work throws and rollback fails' , done => {
157+ testRetryWhenTransactionWorkThrowsAndRollbackFails (
158+ [ SERVICE_UNAVAILABLE , TRANSIENT_ERROR_2 , SESSION_EXPIRED , SESSION_EXPIRED ] ,
159+ [ SESSION_EXPIRED , TRANSIENT_ERROR_1 ] ,
160+ done
161+ ) ;
162+ } ) ;
163+
155164 it ( 'should cancel in-flight timeouts when closed' , done => {
156165 const executor = new TransactionExecutor ( ) ;
157166 // do not execute setTimeout callbacks
@@ -190,16 +199,16 @@ describe('TransactionExecutor', () => {
190199 function testRetryWhenTransactionCreatorFails ( errorCodes , done ) {
191200 const executor = new TransactionExecutor ( ) ;
192201 const transactionCreator = throwingTransactionCreator ( errorCodes , new FakeTransaction ( ) ) ;
193- let workInvocationCounter = 0 ;
202+ const usedTransactions = [ ] ;
194203
195204 const result = executor . execute ( transactionCreator , tx => {
196205 expect ( tx ) . toBeDefined ( ) ;
197- workInvocationCounter ++ ;
206+ usedTransactions . push ( tx ) ;
198207 return Promise . resolve ( 42 ) ;
199208 } ) ;
200209
201210 result . then ( value => {
202- expect ( workInvocationCounter ) . toEqual ( 1 ) ;
211+ expect ( usedTransactions . length ) . toEqual ( 1 ) ;
203212 expect ( value ) . toEqual ( 42 ) ;
204213 verifyRetryDelays ( fakeSetTimeout , errorCodes . length ) ;
205214 done ( ) ;
@@ -208,18 +217,19 @@ describe('TransactionExecutor', () => {
208217
209218 function testRetryWhenTransactionWorkReturnsRejectedPromise ( errorCodes , done ) {
210219 const executor = new TransactionExecutor ( ) ;
211- let workInvocationCounter = 0 ;
220+ const usedTransactions = [ ] ;
212221 const realWork = transactionWork ( errorCodes , 42 ) ;
213222
214223 const result = executor . execute ( transactionCreator ( ) , tx => {
215224 expect ( tx ) . toBeDefined ( ) ;
216- workInvocationCounter ++ ;
225+ usedTransactions . push ( tx ) ;
217226 return realWork ( ) ;
218227 } ) ;
219228
220229 result . then ( value => {
221230 // work should have failed 'failures.length' times and succeeded 1 time
222- expect ( workInvocationCounter ) . toEqual ( errorCodes . length + 1 ) ;
231+ expect ( usedTransactions . length ) . toEqual ( errorCodes . length + 1 ) ;
232+ expectAllTransactionsToBeClosed ( usedTransactions ) ;
223233 expect ( value ) . toEqual ( 42 ) ;
224234 verifyRetryDelays ( fakeSetTimeout , errorCodes . length ) ;
225235 done ( ) ;
@@ -228,18 +238,19 @@ describe('TransactionExecutor', () => {
228238
229239 function testRetryWhenTransactionCommitReturnsRejectedPromise ( errorCodes , done ) {
230240 const executor = new TransactionExecutor ( ) ;
231- let workInvocationCounter = 0 ;
241+ const usedTransactions = [ ] ;
232242 const realWork = ( ) => Promise . resolve ( 4242 ) ;
233243
234244 const result = executor . execute ( transactionCreator ( errorCodes ) , tx => {
235245 expect ( tx ) . toBeDefined ( ) ;
236- workInvocationCounter ++ ;
246+ usedTransactions . push ( tx ) ;
237247 return realWork ( ) ;
238248 } ) ;
239249
240250 result . then ( value => {
241251 // work should have failed 'failures.length' times and succeeded 1 time
242- expect ( workInvocationCounter ) . toEqual ( errorCodes . length + 1 ) ;
252+ expect ( usedTransactions . length ) . toEqual ( errorCodes . length + 1 ) ;
253+ expectAllTransactionsToBeClosed ( usedTransactions ) ;
243254 expect ( value ) . toEqual ( 4242 ) ;
244255 verifyRetryDelays ( fakeSetTimeout , errorCodes . length ) ;
245256 done ( ) ;
@@ -248,37 +259,60 @@ describe('TransactionExecutor', () => {
248259
249260 function testRetryWhenTransactionWorkThrows ( errorCodes , done ) {
250261 const executor = new TransactionExecutor ( ) ;
251- let workInvocationCounter = 0 ;
262+ const usedTransactions = [ ] ;
252263 const realWork = throwingTransactionWork ( errorCodes , 42 ) ;
253264
254265 const result = executor . execute ( transactionCreator ( ) , tx => {
255266 expect ( tx ) . toBeDefined ( ) ;
256- workInvocationCounter ++ ;
267+ usedTransactions . push ( tx ) ;
257268 return realWork ( ) ;
258269 } ) ;
259270
260271 result . then ( value => {
261272 // work should have failed 'failures.length' times and succeeded 1 time
262- expect ( workInvocationCounter ) . toEqual ( errorCodes . length + 1 ) ;
273+ expect ( usedTransactions . length ) . toEqual ( errorCodes . length + 1 ) ;
274+ expectAllTransactionsToBeClosed ( usedTransactions ) ;
263275 expect ( value ) . toEqual ( 42 ) ;
264276 verifyRetryDelays ( fakeSetTimeout , errorCodes . length ) ;
265277 done ( ) ;
266278 } ) ;
267279 }
268280
281+ function testRetryWhenTransactionWorkThrowsAndRollbackFails ( txWorkErrorCodes , rollbackErrorCodes , done ) {
282+ const executor = new TransactionExecutor ( ) ;
283+ const usedTransactions = [ ] ;
284+ const realWork = throwingTransactionWork ( txWorkErrorCodes , 424242 ) ;
285+
286+ const result = executor . execute ( transactionCreator ( [ ] , rollbackErrorCodes ) , tx => {
287+ expect ( tx ) . toBeDefined ( ) ;
288+ usedTransactions . push ( tx ) ;
289+ return realWork ( ) ;
290+ } ) ;
291+
292+ result . then ( value => {
293+ // work should have failed 'failures.length' times and succeeded 1 time
294+ expect ( usedTransactions . length ) . toEqual ( txWorkErrorCodes . length + 1 ) ;
295+ expectAllTransactionsToBeClosed ( usedTransactions ) ;
296+ expect ( value ) . toEqual ( 424242 ) ;
297+ verifyRetryDelays ( fakeSetTimeout , txWorkErrorCodes . length ) ;
298+ done ( ) ;
299+ } ) ;
300+ }
301+
269302 function testNoRetryOnUnknownError ( errorCodes , expectedWorkInvocationCount , done ) {
270303 const executor = new TransactionExecutor ( ) ;
271- let workInvocationCounter = 0 ;
304+ const usedTransactions = [ ] ;
272305 const realWork = transactionWork ( errorCodes , 42 ) ;
273306
274307 const result = executor . execute ( transactionCreator ( ) , tx => {
275308 expect ( tx ) . toBeDefined ( ) ;
276- workInvocationCounter ++ ;
309+ usedTransactions . push ( tx ) ;
277310 return realWork ( ) ;
278311 } ) ;
279312
280313 result . catch ( error => {
281- expect ( workInvocationCounter ) . toEqual ( expectedWorkInvocationCount ) ;
314+ expect ( usedTransactions . length ) . toEqual ( expectedWorkInvocationCount ) ;
315+ expectAllTransactionsToBeClosed ( usedTransactions ) ;
282316 if ( errorCodes . length === 1 ) {
283317 expect ( error . code ) . toEqual ( errorCodes [ 0 ] ) ;
284318 } else {
@@ -290,9 +324,10 @@ describe('TransactionExecutor', () => {
290324
291325} ) ;
292326
293- function transactionCreator ( commitErrorCodes ) {
294- const remainingErrorCodes = ( commitErrorCodes || [ ] ) . slice ( ) . reverse ( ) ;
295- return ( ) => new FakeTransaction ( remainingErrorCodes . pop ( ) ) ;
327+ function transactionCreator ( commitErrorCodes , rollbackErrorCodes ) {
328+ const remainingCommitErrorCodes = ( commitErrorCodes || [ ] ) . slice ( ) . reverse ( ) ;
329+ const remainingRollbackErrorCodes = ( rollbackErrorCodes || [ ] ) . slice ( ) . reverse ( ) ;
330+ return ( ) => new FakeTransaction ( remainingCommitErrorCodes . pop ( ) , remainingRollbackErrorCodes . pop ( ) ) ;
296331}
297332
298333function throwingTransactionCreator ( errorCodes , result ) {
@@ -348,20 +383,35 @@ function verifyRetryDelays(fakeSetTimeout, expectedInvocationCount) {
348383 } ) ;
349384}
350385
386+ function expectAllTransactionsToBeClosed ( transactions ) {
387+ transactions . forEach ( tx => expect ( tx . isOpen ( ) ) . toBeFalsy ( ) ) ;
388+ }
389+
351390class FakeTransaction {
352391
353- constructor ( commitErrorCode ) {
392+ constructor ( commitErrorCode , rollbackErrorCode ) {
354393 this . _commitErrorCode = commitErrorCode ;
394+ this . _rollbackErrorCode = rollbackErrorCode ;
395+ this . _open = true ;
355396 }
356397
357398 isOpen ( ) {
358- return true ;
399+ return this . _open ;
359400 }
360401
361402 commit ( ) {
403+ this . _open = false ;
362404 if ( this . _commitErrorCode ) {
363405 return Promise . reject ( error ( this . _commitErrorCode ) ) ;
364406 }
365407 return Promise . resolve ( ) ;
366408 }
409+
410+ rollback ( ) {
411+ this . _open = false ;
412+ if ( this . _rollbackErrorCode ) {
413+ return Promise . reject ( error ( this . _rollbackErrorCode ) ) ;
414+ }
415+ return Promise . resolve ( ) ;
416+ }
367417}
0 commit comments