0

Say we have a simple node.js transform stream:

export class JSONParser extends stream.Transform {

  constructor() {
    super({objectMode: true});
  }
}

I want to process items synchronously for awhile and then delay the remainder. Something like this:

export class JSONParser extends stream.Transform {

  count = 0;

  constructor() {
    super({objectMode: true});
  }

  _transform(chunk, encoding, cb) {

   const modifiedChunk = this.modify(chunk);

   if(count++ % 55 === 0){
     process.nextTick(() => this.push(modifiedChunk));
     return;
   }

     this.push(modifiedChunk);
  }
}

in theory this means that for every 55 items or so, the stream will wait to the next tick to process the remaining items. Question -

  1. will this indeed delay process of all remaining items, or just this one chunk? Will it preserve order of the chunks that get pushed?

  2. I believe a token bucket algorithm can do rate limiting, and maybe that's a better way to achieve a non-event-loop blocking stream?

hackr
  • 79
  • 7

3 Answers3

0

process.nextTick is the async method. It will invoke the method you have passed after the existing process stack is complete.

So the order of the items which will get pushed will be (assuming there are 112 items):

1,2,3,4...54, 56, 57,..., 109, 111, 112, 55, 110

  • actually the order will be preserved, since the next item waits for the callback to fire, not 100% intutive, but I am glad I figured it out eventualy –  Aug 10 '19 at 01:14
  • The order will be preserved for all elements at positions 55, 110, etc.. However, if you change the condition to if(count++ > 55), then it will do for all the items. – Mohit Gupta Aug 12 '19 at 03:49
0

In this case it's better to use setImmediate not process.nextTick as the latter will starve I/O:

   if(count++ % 55 === 0){
     setImmediate(() => this.push(modifiedChunk));
     return;
   }

instead of:

   if(count++ % 55 === 0){
     process.nextTick(() => this.push(modifiedChunk));
     return;
   }
0

I'll try to answer your questions and explain why:

  1. Will this indeed delay process of all remaining items, or just this one chunk? Will it preserve order of the chunks that get pushed?

Yes it will, but you need to make a small correction. The transform stream will not call the _transform method until push cb is called, however - please see that you don't actually call the cb at all. You should do so after the stream is good to process the next chunk:

  _transform(chunk, encoding, cb) {

   const modifiedChunk = this.modify(chunk);

    if(count++ % 55 === 0){
      process.nextTick(() => {
        this.push(modifiedChunk); 
        cb()
      });
      return;
    }

    this.push(modifiedChunk);
    cb();

  }
  1. I believe a token bucket algorithm can do rate limiting, and maybe that's a better way to achieve a non-event-loop blocking stream?

The algorithm you wrote doesn't seem to do actual rate limiting - at least not in a sense of chunks per second. It's only deferring some processing to then next tick every this number of chunks.

Token bucket would be a good solution and you could even create a simple PassThrough stream with a transform method like this:

new PassThrough({transform(...args) {
   // some logic to see if you can push out some chunks
   // if so:
   return super._transform(...args)
   // otherwise
   return bucket.ready().then(() => super._transform(...args));
}   

If you need some ideas, here's an example of rate limiting I implemented scramjet. It's similar in operation to token bucket but it's time based, not bucket size based - although I think it accounts to the same result.

Michał Karpacki
  • 2,588
  • 21
  • 34