@@ -19,7 +19,6 @@ import * as protos from '../../protos/protos';
1919import { WriterClient } from './writer_client' ;
2020import { PendingWrite } from './pending_write' ;
2121import { logger } from './logger' ;
22- import { parseStorageErrors } from './error' ;
2322
2423type TableSchema = protos . google . cloud . bigquery . storage . v1 . ITableSchema ;
2524type IInt64Value = protos . google . protobuf . IInt64Value ;
@@ -56,6 +55,7 @@ export class StreamConnection extends EventEmitter {
5655 private _streamId : string ;
5756 private _writeClient : WriterClient ;
5857 private _connection ?: gax . CancellableStream | null ;
58+ private _lastConnectionError ?: gax . GoogleError | null ;
5959 private _callOptions ?: gax . CallOptions ;
6060 private _pendingWrites : PendingWrite [ ] ;
6161
@@ -76,6 +76,7 @@ export class StreamConnection extends EventEmitter {
7676 if ( this . isOpen ( ) ) {
7777 this . close ( ) ;
7878 }
79+ this . _lastConnectionError = null ;
7980 const callOptions = this . resolveCallOptions (
8081 this . _streamId ,
8182 this . _callOptions
@@ -86,7 +87,23 @@ export class StreamConnection extends EventEmitter {
8687 this . _connection . on ( 'data' , this . handleData ) ;
8788 this . _connection . on ( 'error' , this . handleError ) ;
8889 this . _connection . on ( 'close' , ( ) => {
89- this . trace ( 'connection closed' ) ;
90+ this . trace ( 'connection closed' , this . _lastConnectionError ) ;
91+ if ( this . hasPendingWrites ( ) ) {
92+ const retrySettings = this . _writeClient . _retrySettings ;
93+ if (
94+ retrySettings . enableWriteRetries &&
95+ this . isRetryableError ( this . _lastConnectionError )
96+ ) {
97+ this . reconnect ( ) ;
98+ this . resendAllPendingWrites ( ) ;
99+ } else {
100+ const err = new gax . GoogleError (
101+ 'Connection failure, please retry the request'
102+ ) ;
103+ err . code = gax . Status . UNAVAILABLE ;
104+ this . ackAllPendingWrites ( err ) ;
105+ }
106+ }
90107 } ) ;
91108 this . _connection . on ( 'pause' , ( ) => {
92109 this . trace ( 'connection paused' ) ;
@@ -106,62 +123,53 @@ export class StreamConnection extends EventEmitter {
106123
107124 private handleError = ( err : gax . GoogleError ) => {
108125 this . trace ( 'on error' , err , JSON . stringify ( err ) ) ;
109- if ( this . shouldReconnect ( err ) ) {
110- this . reconnect ( ) ;
111- return ;
112- }
113- let nextPendingWrite = this . getNextPendingWrite ( ) ;
114- if ( this . isPermanentError ( err ) ) {
115- this . trace ( 'found permanent error' , err ) ;
116- while ( nextPendingWrite ) {
117- this . ackNextPendingWrite ( err ) ;
118- nextPendingWrite = this . getNextPendingWrite ( ) ;
119- }
120- this . emit ( 'error' , err ) ;
121- return ;
122- }
123- if ( this . isRequestError ( err ) && nextPendingWrite ) {
126+ this . _lastConnectionError = err ;
127+ const nextPendingWrite = this . getNextPendingWrite ( ) ;
128+ if ( nextPendingWrite ) {
124129 this . trace (
125130 'found request error with pending write' ,
126131 err ,
127132 nextPendingWrite
128133 ) ;
129- this . ackNextPendingWrite ( err ) ;
134+ this . handleRetry ( err ) ;
135+ }
136+ if ( this . listenerCount ( 'error' ) === 0 && this . isRetryableError ( err ) ) {
130137 return ;
131138 }
132139 this . emit ( 'error' , err ) ;
133140 } ;
134141
135- private shouldReconnect ( err : gax . GoogleError ) : boolean {
136- const reconnectionErrorCodes = [
137- gax . Status . UNAVAILABLE ,
138- gax . Status . RESOURCE_EXHAUSTED ,
142+ private handleRetry ( err : gax . GoogleError ) {
143+ const retrySettings = this . _writeClient . _retrySettings ;
144+ if ( retrySettings . enableWriteRetries && this . isRetryableError ( err ) ) {
145+ if ( ! this . isConnectionClosed ( ) ) {
146+ const pw = this . _pendingWrites . pop ( ) ! ;
147+ this . send ( pw ) ;
148+ }
149+ } else {
150+ this . ackNextPendingWrite ( err ) ;
151+ }
152+ }
153+
154+ private isRetryableError ( err ?: gax . GoogleError | null ) : boolean {
155+ if ( ! err ) {
156+ return false ;
157+ }
158+ const errorCodes = [
139159 gax . Status . ABORTED ,
160+ gax . Status . UNAVAILABLE ,
140161 gax . Status . CANCELLED ,
141- gax . Status . DEADLINE_EXCEEDED ,
142162 gax . Status . INTERNAL ,
163+ gax . Status . DEADLINE_EXCEEDED ,
143164 ] ;
144- return ! ! err . code && reconnectionErrorCodes . includes ( err . code ) ;
165+ return ! ! err . code && errorCodes . includes ( err . code ) ;
145166 }
146167
147- private isPermanentError ( err : gax . GoogleError ) : boolean {
148- if ( err . code === gax . Status . INVALID_ARGUMENT ) {
149- const storageErrors = parseStorageErrors ( err ) ;
150- for ( const storageError of storageErrors ) {
151- if (
152- storageError . errorMessage ?. includes (
153- 'Schema mismatch due to extra fields in user schema'
154- )
155- ) {
156- return true ;
157- }
158- }
168+ private isConnectionClosed ( ) {
169+ if ( this . _connection ) {
170+ return this . _connection . destroyed || this . _connection . closed ;
159171 }
160- return false ;
161- }
162-
163- private isRequestError ( err : gax . GoogleError ) : boolean {
164- return err . code === gax . Status . INVALID_ARGUMENT ;
172+ return true ;
165173 }
166174
167175 private resolveCallOptions (
@@ -183,15 +191,23 @@ export class StreamConnection extends EventEmitter {
183191 }
184192
185193 private handleData = ( response : AppendRowsResponse ) => {
186- this . trace ( 'data arrived' , response ) ;
187- const pw = this . getNextPendingWrite ( ) ;
188- if ( ! pw ) {
194+ this . trace ( 'data arrived' , response , this . _pendingWrites . length ) ;
195+ if ( ! this . hasPendingWrites ( ) ) {
189196 this . trace ( 'data arrived with no pending write available' , response ) ;
190197 return ;
191198 }
192199 if ( response . updatedSchema ) {
193200 this . emit ( 'schemaUpdated' , response . updatedSchema ) ;
194201 }
202+ const responseErr = response . error ;
203+ if ( responseErr ) {
204+ const gerr = new gax . GoogleError ( responseErr . message ! ) ;
205+ gerr . code = responseErr . code ! ;
206+ if ( this . isRetryableError ( gerr ) ) {
207+ this . handleRetry ( gerr ) ;
208+ return ;
209+ }
210+ }
195211 this . ackNextPendingWrite ( null , response ) ;
196212 } ;
197213
@@ -238,13 +254,38 @@ export class StreamConnection extends EventEmitter {
238254 return this . _streamId ;
239255 } ;
240256
257+ private hasPendingWrites ( ) : boolean {
258+ return this . _pendingWrites . length > 0 ;
259+ }
260+
241261 private getNextPendingWrite ( ) : PendingWrite | null {
242262 if ( this . _pendingWrites . length > 0 ) {
243- return this . _pendingWrites [ 0 ] ;
263+ return this . _pendingWrites [ this . _pendingWrites . length - 1 ] ;
244264 }
245265 return null ;
246266 }
247267
268+ private resendAllPendingWrites ( ) {
269+ const pendingWritesToRetry = [ ...this . _pendingWrites ] ; // copy array;
270+ let pw = pendingWritesToRetry . pop ( ) ;
271+ while ( pw ) {
272+ this . _pendingWrites . pop ( ) ; // remove from real queue
273+ this . send ( pw ) ; // .send immediately adds to the queue
274+ pw = pendingWritesToRetry . pop ( ) ;
275+ }
276+ }
277+
278+ private ackAllPendingWrites (
279+ err : Error | null ,
280+ result ?:
281+ | protos . google . cloud . bigquery . storage . v1 . IAppendRowsResponse
282+ | undefined
283+ ) {
284+ while ( this . hasPendingWrites ( ) ) {
285+ this . ackNextPendingWrite ( err , result ) ;
286+ }
287+ }
288+
248289 private ackNextPendingWrite (
249290 err : Error | null ,
250291 result ?:
@@ -253,6 +294,7 @@ export class StreamConnection extends EventEmitter {
253294 ) {
254295 const pw = this . _pendingWrites . pop ( ) ;
255296 if ( pw ) {
297+ this . trace ( 'ack pending write:' , pw , err , result ) ;
256298 pw . _markDone ( err , result ) ;
257299 }
258300 }
@@ -279,23 +321,27 @@ export class StreamConnection extends EventEmitter {
279321 }
280322
281323 private send ( pw : PendingWrite ) {
282- const request = pw . getRequest ( ) ;
283- if ( ! this . _connection ) {
284- pw . _markDone ( new Error ( 'connection closed' ) ) ;
324+ const retrySettings = this . _writeClient . _retrySettings ;
325+ const tries = pw . _increaseAttempts ( ) ;
326+ if ( tries > retrySettings . maxRetryAttempts ) {
327+ pw . _markDone (
328+ new Error ( `pending write max retries reached: ${ tries } attempts` )
329+ ) ;
285330 return ;
286331 }
287- if ( this . _connection . destroyed || this . _connection . closed ) {
332+ if ( this . isConnectionClosed ( ) ) {
288333 this . reconnect ( ) ;
289334 }
290335 this . trace ( 'sending pending write' , pw ) ;
291336 try {
292- this . _connection . write ( request , err => {
337+ const request = pw . getRequest ( ) ;
338+ this . _pendingWrites . unshift ( pw ) ;
339+ this . _connection ?. write ( request , err => {
293340 this . trace ( 'wrote pending write' , err , this . _pendingWrites . length ) ;
294341 if ( err ) {
295342 pw . _markDone ( err ) ; //TODO: add retries
296343 return ;
297344 }
298- this . _pendingWrites . unshift ( pw ) ;
299345 } ) ;
300346 } catch ( err ) {
301347 pw . _markDone ( err as Error ) ;
@@ -306,14 +352,16 @@ export class StreamConnection extends EventEmitter {
306352 * Check if connection is open and ready to send requests.
307353 */
308354 isOpen ( ) : boolean {
309- return ! ! this . _connection ;
355+ return ! this . isConnectionClosed ( ) ;
310356 }
311357
312358 /**
313- * Reconnect and re send inflight requests .
359+ * Re open appendRows BiDi gRPC connection .
314360 */
315361 reconnect ( ) {
316- this . trace ( 'reconnect called' ) ;
362+ this . trace (
363+ `reconnect called with ${ this . _pendingWrites . length } pending writes`
364+ ) ;
317365 this . close ( ) ;
318366 this . open ( ) ;
319367 }
@@ -347,7 +395,6 @@ export class StreamConnection extends EventEmitter {
347395 async flushRows ( request ?: {
348396 offset ?: IInt64Value [ 'value' ] ;
349397 } ) : Promise < FlushRowsResponse | null > {
350- this . close ( ) ;
351398 if ( this . isDefaultStream ( ) ) {
352399 return null ;
353400 }
0 commit comments