@@ -43,10 +43,16 @@ interface RequestFunction {
4343 * @property {boolean } [json=false] Indicates if the Row objects should be
4444 * formatted into JSON.
4545 * @property {JSONOptions } [jsonOptions] JSON options.
46+ * @property {number } [maxResumeRetries=20] The maximum number of times that the
47+ * stream will retry to push data downstream, when the downstream indicates
48+ * that it is not ready for any more data. Increase this value if you
49+ * experience 'Stream is still not ready to receive data' errors as a
50+ * result of a slow writer in your receiving stream.
4651 */
4752export interface RowOptions {
4853 json ?: boolean ;
4954 jsonOptions ?: JSONOptions ;
55+ maxResumeRetries ?: number ;
5056}
5157
5258/**
@@ -136,11 +142,12 @@ export class PartialResultStream extends Transform implements ResultEvents {
136142 private _options : RowOptions ;
137143 private _pendingValue ?: p . IValue ;
138144 private _values : p . IValue [ ] ;
145+ private _numPushFailed = 0 ;
139146 constructor ( options = { } ) {
140147 super ( { objectMode : true } ) ;
141148
142149 this . _destroyed = false ;
143- this . _options = options ;
150+ this . _options = Object . assign ( { maxResumeRetries : 20 } , options ) ;
144151 this . _values = [ ] ;
145152 }
146153 /**
@@ -187,20 +194,60 @@ export class PartialResultStream extends Transform implements ResultEvents {
187194 . fields as google . spanner . v1 . StructType . Field [ ] ;
188195 }
189196
197+ let res = true ;
190198 if ( ! is . empty ( chunk . values ) ) {
191- this . _addChunk ( chunk ) ;
199+ res = this . _addChunk ( chunk ) ;
192200 }
193201
194- next ( ) ;
202+ if ( res ) {
203+ next ( ) ;
204+ } else {
205+ // Wait a little before we push any more data into the pipeline as a
206+ // component downstream has indicated that a break is needed. Pause the
207+ // request stream to prevent it from filling up the buffer while we are
208+ // waiting.
209+ // The stream will initially pause for 2ms, and then double the pause time
210+ // for each new pause.
211+ const initialPauseMs = 2 ;
212+ setTimeout ( ( ) => {
213+ this . _tryResume ( next , 2 * initialPauseMs ) ;
214+ } , initialPauseMs ) ;
215+ }
216+ }
217+
218+ private _tryResume ( next : Function , timeout : number ) {
219+ // Try to push an empty chunk to check whether more data can be accepted.
220+ if ( this . push ( undefined ) ) {
221+ this . _numPushFailed = 0 ;
222+ this . emit ( 'resumed' ) ;
223+ next ( ) ;
224+ } else {
225+ // Downstream returned false indicating that it is still not ready for
226+ // more data.
227+ this . _numPushFailed ++ ;
228+ if ( this . _numPushFailed === this . _options . maxResumeRetries ) {
229+ this . destroy (
230+ new Error (
231+ `Stream is still not ready to receive data after ${ this . _numPushFailed } attempts to resume.`
232+ )
233+ ) ;
234+ return ;
235+ }
236+ setTimeout ( ( ) => {
237+ const nextTimeout = Math . min ( timeout * 2 , 1024 ) ;
238+ this . _tryResume ( next , nextTimeout ) ;
239+ } , timeout ) ;
240+ }
195241 }
242+
196243 /**
197244 * Manages any chunked values.
198245 *
199246 * @private
200247 *
201248 * @param {object } chunk The partial result set.
202249 */
203- private _addChunk ( chunk : google . spanner . v1 . PartialResultSet ) : void {
250+ private _addChunk ( chunk : google . spanner . v1 . PartialResultSet ) : boolean {
204251 const values : Value [ ] = chunk . values . map ( GrpcService . decodeValue_ ) ;
205252
206253 // If we have a chunk to merge, merge the values now.
@@ -223,7 +270,14 @@ export class PartialResultStream extends Transform implements ResultEvents {
223270 this . _pendingValue = values . pop ( ) ;
224271 }
225272
226- values . forEach ( value => this . _addValue ( value ) ) ;
273+ let res = true ;
274+ values . forEach ( value => {
275+ res = this . _addValue ( value ) && res ;
276+ if ( ! res ) {
277+ this . emit ( 'paused' ) ;
278+ }
279+ } ) ;
280+ return res ;
227281 }
228282 /**
229283 * Manages complete values, pushing a completed row into the stream once all
@@ -233,25 +287,24 @@ export class PartialResultStream extends Transform implements ResultEvents {
233287 *
234288 * @param {* } value The complete value.
235289 */
236- private _addValue ( value : Value ) : void {
290+ private _addValue ( value : Value ) : boolean {
237291 const values = this . _values ;
238292
239293 values . push ( value ) ;
240294
241295 if ( values . length !== this . _fields . length ) {
242- return ;
296+ return true ;
243297 }
244298
245299 this . _values = [ ] ;
246300
247301 const row : Row = this . _createRow ( values ) ;
248302
249303 if ( this . _options . json ) {
250- this . push ( row . toJSON ( this . _options . jsonOptions ) ) ;
251- return ;
304+ return this . push ( row . toJSON ( this . _options . jsonOptions ) ) ;
252305 }
253306
254- this . push ( row ) ;
307+ return this . push ( row ) ;
255308 }
256309 /**
257310 * Converts an array of values into a row.
@@ -373,7 +426,8 @@ export function partialResultStream(
373426 // mergeStream allows multiple streams to be connected into one. This is good;
374427 // if we need to retry a request and pipe more data to the user's stream.
375428 const requestsStream = mergeStream ( ) ;
376- const userStream = streamEvents ( new PartialResultStream ( options ) ) ;
429+ const partialRSStream = new PartialResultStream ( options ) ;
430+ const userStream = streamEvents ( partialRSStream ) ;
377431 const batchAndSplitOnTokenStream = checkpointStream . obj ( {
378432 maxQueued : 10 ,
379433 isCheckpointFn : ( row : google . spanner . v1 . PartialResultSet ) : boolean => {
@@ -433,5 +487,7 @@ export function partialResultStream(
433487 lastResumeToken = row . resumeToken ;
434488 } )
435489 . pipe ( userStream )
490+ . on ( 'paused' , ( ) => requestsStream . pause ( ) )
491+ . on ( 'resumed' , ( ) => requestsStream . resume ( ) )
436492 ) ;
437493}
0 commit comments