Sunday, April 29, 2012

Real SPDY v3 WINDOW_UPDATE

‹prev | My Chain | next›

I ended yesterday in an argument with Chrome over SPDY version 3 WINDOW_UPDATE frames. I thought that sending them immediately after connection should impact the browser's data window. Chrome disagreed.

Upon further reading of the specification, it turns out that I may, in fact, have been mistaken. I have a hard time reading networking specifications sometimes, but I now think that WINDOW_UPDATE is only sent after too much data has already been received.

The scenario from the specification is that the window size is originally the default 64KB, but the recipient immediately replies on connection that it can only handle 16KB. Unlike what I did yesterday, the specification states that the recipient communicates this information with a SETTINGS frame, not a WINDOW_UPDATE. The sender has already sent along the default 64KB, so it now realizes that it has sent 48KB too much. Assuming the recipient does not terminate the session (which is a possibility), then the sender needs to wait for enough WINDOW_UPDATE frames to be sent back to add up to 48kb at which point data can again start flowing.

That seems like a hard scenario to establish when the server is the recipient. The form that I use to submit the data will already have established SETTINGS:


So a race condition of 64KB vs. 16KB seems hard to establish. Besides, I could be completely wrong about this whole thing (again).

So, instead, I specify an absurdly small window size, 1024 bytes, that the server can handle in the initial SETTINGS:
    // ...
    settings = new Buffer(28);

    settings.writeUInt32BE(0x80030004, 0, true); // Version and type
    settings.writeUInt32BE((4 + 8 + 8) & 0x00FFFFFF, 4, true); // length
    settings.writeUInt32BE(0x00000002, 8, true); // Count of entries

    settings.writeUInt32BE(0x01000004, 12, true); // Entry ID and Persist flag
    settings.writeUInt32BE(count, 16, true); // 100 Streams

    settings.writeUInt32BE(0x01000007, 20, true); // Entry ID and Persist flag
    settings.writeUInt32BE(1024 & 0x7fffffff, 24, true); // Window Size (Bytes)
    // ...
The SETTINGS frame goes out when the form is initially served so that, by the time the browser POSTs the form data to the server, both client and server should agree that 1024B is the window size.

Next, after every data frame is received, I lie to the browser that I have only processed half of the 1024B that it sent:
Parser.prototype.execute = function execute(state, data, callback) {
  if (state.type === 'frame-head') { /* ... */  }
  else if (state.type === 'frame-body') {
    var self = this;

    // Data frame
    if (!state.header.control) {
      this.connection.write(
        this.framer.windowUpdateFrame(state.header.id, 512)
      );

      return onFrame(null, {
        type: 'DATA',
        // ...
      });
    } else { /* ... */ }
  }
};
After the browser sends the 1024B window size that the browser and server agreed upon, its window size should be 0. In other words, it should pause until it gets back a WINDOW_UPDATE saying that all or part of the original 1024B has been processed. At this point, the browser's window size will equal what WINDOW_UPDATE says has been processed (the 512B that I lie about).

So I fire up the browser, fill out the form with well over 1024 characters and submit:
SPDY_SESSION_SYN_STREAM
--> flags = 0
--> :host: localhost:3000
    :method: POST
    :path: /cool
    :scheme: https
    :version: HTTP/1.1
    accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
    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: max-age=0
    content-length: 11747
    content-type: application/x-www-form-urlencoded
    origin: https://localhost:3000
    referer: https://localhost:3000/cool
And immediately after the opening SYN_STREAM, the browser sends a 1024B data packet:
SPDY_SESSION_SEND_DATA
--> flags = 0
--> size = 1024
--> stream_id = 9
And then it waits:
SPDY_SESSION_STALLED_ON_SEND_WINDOW
--> stream_id = 9
Meanwhile, the server processes the first data frame and lies that it had only processed 512B:
SPDY_SESSION_RECEIVED_WINDOW_UPDATE
--> delta = 512
--> stream_id = 9
At this point, the browser's internal data window is 512B, which it promptly sends along and then pauses until another WINDOW_UPDATE is sent:
SPDY_SESSION_SEND_DATA
--> flags = 0
--> size = 512
--> stream_id = 9
SPDY_SESSION_STALLED_ON_SEND_WINDOW
--> stream_id = 9
And, when the WINDOW_UPDATE is received, the cycle starts anew:
SPDY_SESSION_RECEIVED_WINDOW_UPDATE
--> delta = 512
--> stream_id = 9
SPDY_SESSION_SEND_DATA
--> flags = 0
--> size = 512
--> stream_id = 9
SPDY_SESSION_STALLED_ON_SEND_WINDOW
--> stream_id = 9
As you might imagine, it takes a number of round trips to complete the conversation. This example is intentionally contrived to see flow control in action. In real-life scenarios, a SPDY stream might pause a much larger window because of a slow DB write or unresponsive cluster node. The nice thing about the per-stream flow control in SPDY is that other streams can proceed like normal and regular TCP/IP flow control can also do its thing for then entire connection.

It would be nice for node-spdy to detect situations in which WINDOW_UPDATE frames are needed. Hard coding them like this is no solution. I also need to get the reverse case working. That is, if the server needs to send a 150KB image back to the browser, the server should wait until receiving WINDOW_UPDATEs. Grist for tomorrow, at least.


Day #371

2 comments:

  1. I think you mean 1024B (and 512B) not KB? If not I'm really confused. Hell, even if it is 1024B I'm still confused, lol.

    ReplyDelete
    Replies
    1. Hah! You are correct, of course. It is 1024B that is absurdly low, not 1024KB. Corrected.

      Thanks!

      Delete