1
1
/* eslint-disable @typescript-eslint/no-shadow */
2
+ import { Cardano , ProviderError , ProviderFailure , TxSubmitProvider } from '@cardano-sdk/core' ;
2
3
import { Channel , Connection , Message , connect } from 'amqplib' ;
3
4
import { Logger , dummyLogger } from 'ts-log' ;
4
- import { ProviderError , ProviderFailure , TxSubmitProvider } from '@cardano-sdk/core' ;
5
- import { TX_SUBMISSION_QUEUE } from './rabbitmqTxSubmitProvider' ;
5
+ import { TX_SUBMISSION_QUEUE , serializeError , waitForPending } from './utils' ;
6
6
7
7
const moduleName = 'TxSubmitWorker' ;
8
8
9
+ type Optional < T , K extends keyof T > = Pick < Partial < T > , K > & Omit < T , K > ;
10
+
9
11
/**
10
12
* Configuration options parameters for the TxSubmitWorker
11
13
*/
@@ -41,7 +43,7 @@ export interface TxSubmitWorkerDependencies {
41
43
/**
42
44
* The logger. Default: silent
43
45
*/
44
- logger ? : Logger ;
46
+ logger : Logger ;
45
47
46
48
/**
47
49
* The provider to use to submit tx
@@ -89,25 +91,34 @@ export class TxSubmitWorker {
89
91
*/
90
92
#dependencies: TxSubmitWorkerDependencies ;
91
93
92
- /**
93
- * The function to call to resolve the start method exit Promise
94
- */
95
- #exitResolver?: ( ) => void ;
96
-
97
94
/**
98
95
* The internal worker status
99
96
*/
100
97
#status: 'connected' | 'connecting' | 'error' | 'idle' = 'idle' ;
101
98
102
99
/**
103
- * @param { TxSubmitWorkerConfig } config The configuration options
104
- * @param { TxSubmitWorkerDependencies } dependencies The dependency objects
100
+ * @param config The configuration options
101
+ * @param dependencies The dependency objects
105
102
*/
106
- constructor ( config : TxSubmitWorkerConfig , dependencies : TxSubmitWorkerDependencies ) {
103
+ constructor ( config : TxSubmitWorkerConfig , dependencies : Optional < TxSubmitWorkerDependencies , 'logger' > ) {
107
104
this . #config = { parallelTxs : 3 , pollingCycle : 500 , ...config } ;
108
105
this . #dependencies = { logger : dummyLogger , ...dependencies } ;
109
106
}
110
107
108
+ /**
109
+ * The common handler for errors
110
+ *
111
+ * @param isAsync flag to identify asynchronous errors
112
+ * @param err the error itself
113
+ */
114
+ private async errorHandler ( isAsync : boolean , err : unknown ) {
115
+ if ( err ) {
116
+ this . logError ( err , isAsync ) ;
117
+ this . #status = 'error' ;
118
+ await this . stop ( ) ;
119
+ }
120
+ }
121
+
111
122
/**
112
123
* Get the status of the worker
113
124
*
@@ -120,126 +131,115 @@ export class TxSubmitWorker {
120
131
/**
121
132
* Starts the worker
122
133
*/
123
- start ( ) {
124
- return new Promise < void > ( async ( resolve , reject ) => {
125
- const closeHandler = async ( isAsync : boolean , err : unknown ) => {
126
- if ( err ) {
127
- this . logError ( err , isAsync ) ;
128
- this . #exitResolver = undefined ;
129
- this . #status = 'error' ;
130
- await this . stop ( ) ;
131
- reject ( err ) ;
132
- }
133
- } ;
134
-
135
- try {
136
- this . #dependencies. logger ! . info ( `${ moduleName } init: checking tx submission provider health status` ) ;
134
+ async start ( ) {
135
+ try {
136
+ this . #dependencies. logger . info ( `${ moduleName } init: checking tx submission provider health status` ) ;
137
137
138
- const { ok } = await this . #dependencies. txSubmitProvider . healthCheck ( ) ;
138
+ const { ok } = await this . #dependencies. txSubmitProvider . healthCheck ( ) ;
139
139
140
- if ( ! ok ) throw new ProviderError ( ProviderFailure . Unhealthy ) ;
140
+ if ( ! ok ) throw new ProviderError ( ProviderFailure . Unhealthy ) ;
141
141
142
- this . #dependencies. logger ! . info ( `${ moduleName } init: opening RabbitMQ connection` ) ;
143
- this . #exitResolver = resolve ;
144
- this . #status = 'connecting' ;
145
- this . #connection = await connect ( this . #config. rabbitmqUrl . toString ( ) ) ;
146
- this . #connection. on ( 'close' , ( error ) => closeHandler ( true , error ) ) ;
142
+ this . #dependencies. logger . info ( `${ moduleName } init: opening RabbitMQ connection` ) ;
143
+ this . #status = 'connecting' ;
144
+ this . #connection = await connect ( this . #config. rabbitmqUrl . toString ( ) ) ;
145
+ this . #connection. on ( 'close' , ( error ) => this . errorHandler ( true , error ) ) ;
147
146
148
- this . #dependencies. logger ! . info ( `${ moduleName } init: opening RabbitMQ channel` ) ;
149
- this . #channel = await this . #connection. createChannel ( ) ;
150
- this . #channel. on ( 'close' , ( error ) => closeHandler ( true , error ) ) ;
147
+ this . #dependencies. logger . info ( `${ moduleName } init: opening RabbitMQ channel` ) ;
148
+ this . #channel = await this . #connection. createChannel ( ) ;
149
+ this . #channel. on ( 'close' , ( error ) => this . errorHandler ( true , error ) ) ;
151
150
152
- this . #dependencies. logger ! . info ( `${ moduleName } init: ensuring RabbitMQ queue` ) ;
153
- await this . #channel. assertQueue ( TX_SUBMISSION_QUEUE ) ;
154
- this . #dependencies. logger ! . info ( `${ moduleName } : init completed` ) ;
151
+ this . #dependencies. logger . info ( `${ moduleName } init: ensuring RabbitMQ queue` ) ;
152
+ await this . #channel. assertQueue ( TX_SUBMISSION_QUEUE ) ;
153
+ this . #dependencies. logger . info ( `${ moduleName } : init completed` ) ;
155
154
156
- if ( this . #config. parallel ) {
157
- this . #dependencies. logger ! . info ( `${ moduleName } : starting parallel mode` ) ;
158
- await this . #channel. prefetch ( this . #config. parallelTxs ! , true ) ;
155
+ if ( this . #config. parallel ) {
156
+ this . #dependencies. logger . info ( `${ moduleName } : starting parallel mode` ) ;
157
+ await this . #channel. prefetch ( this . #config. parallelTxs ! , true ) ;
159
158
160
- const parallelHandler = ( message : Message | null ) => ( message ? this . submitTx ( message ) : null ) ;
161
- const { consumerTag } = await this . #channel. consume ( TX_SUBMISSION_QUEUE , parallelHandler ) ;
159
+ const parallelHandler = ( message : Message | null ) => ( message ? this . submitTx ( message ) : null ) ;
162
160
163
- this . #consumerTag = consumerTag ;
164
- this . #status = 'connected' ;
165
- } else {
166
- this . #dependencies. logger ! . info ( `${ moduleName } : starting serial mode` ) ;
167
- await this . infiniteLoop ( ) ;
168
- }
169
- } catch ( error ) {
170
- await closeHandler ( false , error ) ;
161
+ this . #consumerTag = ( await this . #channel. consume ( TX_SUBMISSION_QUEUE , parallelHandler ) ) . consumerTag ;
162
+ } else {
163
+ this . #dependencies. logger . info ( `${ moduleName } : starting serial mode` ) ;
164
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
165
+ this . infiniteLoop ( ) ;
171
166
}
172
- } ) ;
167
+
168
+ this . #status = 'connected' ;
169
+ } catch ( error ) {
170
+ await this . errorHandler ( false , error ) ;
171
+ if ( error instanceof ProviderError ) throw error ;
172
+ throw new ProviderError ( ProviderFailure . ConnectionFailure , error ) ;
173
+ }
173
174
}
174
175
175
176
/**
176
- * Stops the worker. Once connection shutdown is completed,
177
- * the Promise returned by the start method is resolved as well
177
+ * Stops the worker.
178
178
*/
179
179
async stop ( ) {
180
- // This method needs to call this.#exitResolver at the end.
181
- // Since it may be called more than once simultaneously,
182
- // we need to ensure this.#exitResolver is called only once,
183
- // so we immediately store its value in a local variable and we reset it
184
- const exitResolver = this . #exitResolver;
185
- this . #exitResolver = undefined ;
186
-
187
- try {
188
- this . #dependencies. logger ! . info ( `${ moduleName } shutdown: closing RabbitMQ channel` ) ;
180
+ this . #dependencies. logger . info ( `${ moduleName } shutdown: closing RabbitMQ channel` ) ;
189
181
182
+ // In case of parallel worker; first of all cancel the consumer
183
+ if ( this . #consumerTag)
190
184
try {
191
- if ( this . #consumerTag) {
192
- const consumerTag = this . #consumerTag;
193
- this . #consumerTag = undefined ;
185
+ // Let's immediately reset this.#consumerTag to be sure the cancel operation is called
186
+ // only once even if the this.stop methond is called more than once
187
+ const consumerTag = this . #consumerTag;
188
+ this . #consumerTag = undefined ;
194
189
195
- await this . #channel?. cancel ( consumerTag ) ;
196
- }
190
+ await this . #channel! . cancel ( consumerTag ) ;
197
191
} catch ( error ) {
198
192
this . logError ( error ) ;
199
193
}
194
+ // In case of serial worker; just instruct the infinite loop it can exit
195
+ else this . #continueForever = false ;
200
196
201
- this . #dependencies. logger ! . info ( `${ moduleName } shutdown: closing RabbitMQ connection` ) ;
197
+ // Wait for pending operations before closing
198
+ await waitForPending ( this . #channel) ;
202
199
203
- try {
204
- await this . #connection ?. close ( ) ;
205
- } catch ( error ) {
206
- this . logError ( error ) ;
207
- }
200
+ try {
201
+ await this . #channel ?. close ( ) ;
202
+ } catch ( error ) {
203
+ this . logError ( error ) ;
204
+ }
208
205
209
- this . #dependencies. logger ! . info ( `${ moduleName } : shutdown completed` ) ;
210
- this . #channel = undefined ;
211
- this . #connection = undefined ;
212
- this . #consumerTag = undefined ;
213
- this . #continueForever = false ;
214
- this . #status = 'idle' ;
215
- } finally {
216
- // Only logging functions could throw an error here...
217
- // Although this is almost impossible, we want to be sure exitResolver is called
218
- exitResolver ?.( ) ;
206
+ this . #dependencies. logger . info ( `${ moduleName } shutdown: closing RabbitMQ connection` ) ;
207
+
208
+ try {
209
+ await this . #connection?. close ( ) ;
210
+ } catch ( error ) {
211
+ this . logError ( error ) ;
219
212
}
213
+
214
+ this . #dependencies. logger . info ( `${ moduleName } : shutdown completed` ) ;
215
+ this . #channel = undefined ;
216
+ this . #connection = undefined ;
217
+ this . #status = 'idle' ;
220
218
}
221
219
222
220
/**
223
221
* Wrapper to log errors from try/catch blocks
224
222
*
225
- * @param {any } error the error to log
223
+ * @param error the error to log
224
+ * @param isAsync flag to set in case the error is asynchronous
225
+ * @param asWarning flag to log the error with warning loglevel
226
226
*/
227
- private logError ( error : unknown , isAsync = false ) {
227
+ private logError ( error : unknown , isAsync = false , asWarning = false ) {
228
228
const errorMessage =
229
229
// eslint-disable-next-line prettier/prettier
230
230
error instanceof Error ? error . message : ( typeof error === 'string' ? error : JSON . stringify ( error ) ) ;
231
231
const errorObject = { error : error instanceof Error ? error . name : 'Unknown error' , isAsync, module : moduleName } ;
232
232
233
- this . #dependencies. logger ! . error ( errorObject , errorMessage ) ;
234
- if ( error instanceof Error ) this . #dependencies. logger ! . debug ( `${ moduleName } :` , error . stack ) ;
233
+ if ( asWarning ) this . #dependencies. logger . warn ( errorObject , errorMessage ) ;
234
+ else this . #dependencies. logger . error ( errorObject , errorMessage ) ;
235
+ if ( error instanceof Error ) this . #dependencies. logger . debug ( `${ moduleName } :` , error . stack ) ;
235
236
}
236
237
237
238
/**
238
239
* The infinite loop to perform serial tx submission
239
240
*/
240
241
private async infiniteLoop ( ) {
241
242
this . #continueForever = true ;
242
- this . #status = 'connected' ;
243
243
244
244
while ( this . #continueForever) {
245
245
const message = await this . #channel?. get ( TX_SUBMISSION_QUEUE ) ;
@@ -254,29 +254,70 @@ export class TxSubmitWorker {
254
254
/**
255
255
* Submit a tx to the provider and ack (or nack) the message
256
256
*
257
- * @param { Message } message the message containing the transaction
257
+ * @param message the message containing the transaction
258
258
*/
259
259
private async submitTx ( message : Message ) {
260
+ const counter = ++ this . #counter;
261
+ let isRetriable = false ;
262
+ let serializableError : unknown ;
263
+ let txId = '' ;
264
+
260
265
try {
261
- const counter = ++ this . #counter;
262
266
const { content } = message ;
267
+ const txBody = new Uint8Array ( content ) ;
268
+
269
+ // Register the handling of current transaction
270
+ txId = Cardano . util . deserializeTx ( txBody ) . id . toString ( ) ;
263
271
264
- this . #dependencies. logger ! . info ( `${ moduleName } : submitting tx` ) ;
265
- this . #dependencies. logger ! . debug ( `${ moduleName } : tx ${ counter } dump:` , content . toString ( 'hex' ) ) ;
266
- await this . #dependencies. txSubmitProvider . submitTx ( new Uint8Array ( content ) ) ;
272
+ this . #dependencies. logger . info ( `${ moduleName } : submitting tx # ${ counter } id: ${ txId } ` ) ;
273
+ this . #dependencies. logger . debug ( `${ moduleName } : tx # ${ counter } dump:` , content . toString ( 'hex' ) ) ;
274
+ await this . #dependencies. txSubmitProvider . submitTx ( txBody ) ;
267
275
268
- this . #dependencies. logger ! . debug ( `${ moduleName } : ACKing RabbitMQ message ${ counter } ` ) ;
276
+ this . #dependencies. logger . debug ( `${ moduleName } : ACKing RabbitMQ message # ${ counter } ` ) ;
269
277
this . #channel?. ack ( message ) ;
270
278
} catch ( error ) {
271
- this . logError ( error ) ;
272
-
273
- try {
274
- this . #dependencies. logger ! . info ( `${ moduleName } : NACKing RabbitMQ message` ) ;
275
- this . #channel?. nack ( message ) ;
276
- // eslint-disable-next-line no-catch-shadow
277
- } catch ( error ) {
278
- this . logError ( error ) ;
279
+ ( { isRetriable, serializableError } = await this . submitTxErrorHandler ( error , counter , message ) ) ;
280
+ } finally {
281
+ // If there is no error or the error can't be retried
282
+ if ( ! serializableError || ! isRetriable ) {
283
+ // Send the response to the original submitter
284
+ try {
285
+ // An empty response message means succesful submission
286
+ const message = serializableError || { } ;
287
+ await this . #channel! . assertQueue ( txId ) ;
288
+ this . logError ( `${ moduleName } : sending response for message #${ counter } ` ) ;
289
+ this . #channel! . sendToQueue ( txId , Buffer . from ( JSON . stringify ( message ) ) ) ;
290
+ } catch ( error ) {
291
+ this . logError ( `${ moduleName } : while sending response for message #${ counter } ` ) ;
292
+ this . logError ( error ) ;
293
+ }
279
294
}
280
295
}
281
296
}
297
+
298
+ /**
299
+ * The error handler of submitTx method
300
+ */
301
+ private async submitTxErrorHandler ( err : unknown , counter : number , message : Message ) {
302
+ const { isRetriable, serializableError } = serializeError ( err ) ;
303
+
304
+ if ( isRetriable ) this . #dependencies. logger . warn ( `${ moduleName } : submitting tx #${ counter } ` ) ;
305
+ else this . #dependencies. logger . error ( `${ moduleName } : submitting tx #${ counter } ` ) ;
306
+ this . logError ( err , false , isRetriable ) ;
307
+
308
+ const action = `${ isRetriable ? 'N' : '' } ACKing RabbitMQ message #${ counter } ` ;
309
+
310
+ try {
311
+ this . #dependencies. logger . info ( `${ moduleName } : ${ action } ` ) ;
312
+ // In RabbitMQ languange, NACKing a message means to ask to retry for it
313
+ // We NACK only those messages which had an error which can be retried
314
+ if ( isRetriable ) this . #channel?. nack ( message ) ;
315
+ else this . #channel?. ack ( message ) ;
316
+ } catch ( error ) {
317
+ this . logError ( `${ moduleName } : while ${ action } ` ) ;
318
+ this . logError ( error ) ;
319
+ }
320
+
321
+ return { isRetriable, serializableError } ;
322
+ }
282
323
}
0 commit comments