Retry-capable trigger service -- with transactions

A few years back, I had a discussion about the correct way to build a retry-capable IS Trigger Service:

Recently, I was building a retry-capable IS trigger service where the steps involved a transaction, and I found some trickiness to it - so I thought I’d put what I learned out here.

A generic outline of the ‘Retry-capable trigger service – with transactions’ service is below.

Service Structure

Try/Catch - Outer Sequence
	Try Sequence
		<Work outside of transaction boundary> 
		call startTransaction (e.g., insert multiple records into database)
		<Work within the transaction boundary> 
		call commitTransaction
	Catch Sequence
		call getLastError
		If pipeline indicates transaction started ...
		 ...(e.g. BRANCH on /lastError/pipeline/startTransactionOutput  == /.+/)
			call rollbackTransaction 
		If Exception is recoverable (e.g., database  down)
			Set 'error' flag to &#8216;true&#8217; 
		If Exception is irrecoverable (e.g., database constraint violated) 
			EXIT with failure (logs pipeline in PRT) 
BRANCH on 'error' flag
        If 'true', throw pub.flow:throwExceptionForRetry

The service does its work in a transaction in the ‘Try’ block. If an exception is thrown, control passes to the ‘Catch’ block where a BRANCH (immediately after ‘getLastError’) calls ‘rollbackTransaction’ to safely roll back the transaction without interfering with the retry mechanism.

The ‘rollbackTransaction’ call must be placed here. This is because it not only rolls back the transaction, but also seems to roll back the state of the pipeline to before ‘startTransaction’ (in the ‘Try’ block).

If rollback is invoked further down the ‘Catch’ block (e.g. after setting the ‘error’ variable),
it destroys pipeline variables created after ‘startTransaction’ (e.g. the ‘error’ variable). This
consequently breaks the service retry process (i.e. ‘throwExceptionForRetry’ does not get called.)