Thursday, May 3, 2012

Reusable Flow Control in SPDY/3

‹prev | My Chain | next›

Yesterday I finally got flow control working working in node-spdy. I had to hard code everything to make it work, but work it did.

Flow control is new in version 3 of the SPDY protocol. It promises to be one of those things that is of great benefit to the protocol by virtue of being a lesson learned in production at Google. The idea is that, certain frames in SPDY could benefit from throttling if they threaten to overwhelm the recipient or other streams within the SPDY session. This is above and beyond the flow control built in to TCP/IP, which will still operate over the course of the entire session.

Anyhow, it's an easy enough concept, but proves fun to implement. At least for me. As mentioned, I hard coded a ton, including the stream ID of the stream used to transfer the overly large image in my contrived scenario. I am also using a single global array to hold the excess data in the data window when the server is forced to buffer data before sending it on to the client.

Tonight, I am going to try to remove some of the hard coding. And most, if not all of the console.log() statements that I have left scattered about.

Now that I have taken a step back from the morass of just trying to get it to work, I find that I may have been making this way too hard on myself. Each stream in a SPDY session has its own internal data window. In desperation, I had made it a global variable—I knew was not a viable solution, but now it seems that I had to do extra work to support it:
var internal_window = Math.pow(2,16);
var backlog = [];

Stream.prototype._writeData = function _writeData(fin, buffer) {
  if (this.id == 7 && internal_window > 0)
    internal_window = internal_window - buffer.length;

  // ...
}
That this.id == 7 check was done to enforce the stream data transfer window only for the stream that I knew would carry the image (learned from experience). In effect, I tried to make the global apply only in one specific case. It is amazing the crazy stuff that I do when in a morass of just trying to get it to work. Anyhow....

I can move the internal_window (size) and the associated backlog into the Stream constructor:
function Stream(connection, frame) {
  // ...

  // Lock data
  this.locked = false;
  this.lockBuffer = [];

  // Store id
  this.id = frame.id;
  // ...

  this._paused = false;
  this._buffer = [];

  this.internalWindowSize = Math.pow(2,16);
  this._backlog = [];

  //...
}
Very shortly, I need to figure out what the lockBuffer and _buffer are. If I can make use of them without a new storage property, I will. For now, I just hope to make this work.

After replacing all references to the internal_window and backlog globals, I restart my spdy/3 express.js application, reload and... it still works! I still get my huge image jammed into my spdy/3 page:

(when it does not work, the image tends to be brokes or partially loaded)

What really ensures that this is correct, however, is that not only does this work on load, but also reload, where the stream ID is no longer 7:
SPDY_SESSION_SYN_STREAM
--> flags = 1
--> :host: localhost:3000
    :method: GET
    :path: /images/pipelining.jpg
    :scheme: https
    :version: HTTP/1.1
    accept: */*
    accept-charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3
    accept-encoding: gzip,deflate,sdch
    accept-language: en-US,en;q=0.8
    cache-control: no-cache
    pragma: no-cache
    referer: https://localhost:3000/
--> id = 17
SPDY_SESSION_SYN_REPLY
--> flags = 0
--> :status: 200
    :version: HTTP/1.1
--> id = 17
SPDY_SESSION_RECV_DATA
--> flags = 0
--> size = 1300
--> stream_id = 17
SPDY_SESSION_RECV_DATA
--> flags = 0
--> size = 1300
--> stream_id = 17

...

SPDY_SESSION_RECV_DATA
--> flags = 0
--> size = 1300
--> stream_id = 17
SPDY_SESSION_SENT_WINDOW_UPDATE
--> delta = 43560
--> stream_id = 17
SPDY_SESSION_RECV_DATA
--> flags = 0
--> size = 1300
--> stream_id = 17

...


SPDY_SESSION_RECV_DATA
--> flags = 0
--> size = 1300
--> stream_id = 17
SPDY_SESSION_RECV_DATA
--> flags = 0
--> size = 369
--> stream_id = 17
SPDY_SESSION_RECV_DATA
--> flags = 0
--> size = 0
--> stream_id = 17
The initial SYN_STREAM comes in to request the image and the server replies with a SYN_REPLY. Over the course of sending data back to the browser, the server exhausts the data window for this stream and waits patiently for WINDOW_UDPATE frames. And node-spdy now handles them with aplomb. Eventually, all of the data is transferred and the server sends the final FIN frame for this stream and all is well.

Last up tonight, I try adding a second huge image the page. Happily, this does not break my solution as both images still display:


Looking through Chrome's SPDY tab, I see the browser receiving the data from the first image and, asynchronously notifying the server that it is ready for more of the second image:
...
SPDY_SESSION_RECV_DATA
--> flags = 0
--> size = 660
--> stream_id = 7
SPDY_SESSION_RECV_DATA
--> flags = 0
--> size = 1300
--> stream_id = 7
SPDY_SESSION_RECV_DATA
--> flags = 0
--> size = 1300
--> stream_id = 7
SPDY_SESSION_SENT_WINDOW_UPDATE
--> delta = 51360
--> stream_id = 9
SPDY_SESSION_RECV_DATA
--> flags = 0
--> size = 1300
--> stream_id = 7
SPDY_SESSION_RECV_DATA
--> flags = 0
--> size = 1300
--> stream_id = 7
...
After the server exhausts the internal data window for the first image, it stops sending that image and resumes sending the second (which it can do because the browser used a WINDOW_UPDATE to signal its readiness):
...
SPDY_SESSION_RECV_DATA
--> flags = 0
--> size = 1300
--> stream_id = 7
SPDY_SESSION_RECV_DATA
--> flags = 0
--> size = 1300
--> stream_id = 9
SPDY_SESSION_RECV_DATA
--> flags = 0
--> size = 1300
--> stream_id = 9
...
Eventually the browser notifies the server that it is ready for more of stream #7 and stream #9 reaches its data window limit and the two switch. This pattern continues all the way until both images are entirely transferred.

So yay! I have a working spdy/3 flow control solution in the spdy-v3 branch of node-spdy. Feel free to kick the tires.

Work still remains. I need to examine the code to see if some of the existing properties might be reusable here. I also need to check that I am closing the connection properly on DATA FIN—I prevented the normal close to allow the internal data window to clear. Lastly, it is possible for the client and server to agree upon data window sizes other than 64kb, so I will need to remove that hard-coded value.

Still, this is a happy stopping point.


Day #375

No comments:

Post a Comment