Sunday, July 3, 2011

SPDY Post-Response Data Push

‹prev | My Chain | next›

I head back into node.js land tonight. I am still at a bit of a loss for how I would like the various SPDY server push callback APIs to work in node-spdy.

In my post-response-push branch, I currently require API calls of the form:
var app = module.exports = express.createServer({
// ...
push: function(pusher) {
// Only push in response to the first request
if (pusher.streamID > 1) return;

pusher.push_file("public/stylesheets/style.css", "https://localhost:3000/stylesheets/style.css");
},
push_after: function(pusher) {
// Only push in response to the first request
if (pusher.streamID > 1) return;

pusher.push_file("public/one.html", "https://localhost:3000/one.html");
pusher.push_file("public/two.html", "https://localhost:3000/two.html");
pusher.push_file("public/three.html", "https://localhost:3000/three.html");
}
});
The push callback pushes data out before the response is send to the browser. The push_after callback is invoked after the response is sent back.

All things considered, that external API is not too bad (though admittedly the underlying code is a mess). I have written some vow.js tests to cover both cases. But I still can't quite settle on how I want the underlying code to work and I am still not 100% settled on that external API. So I had put it aside for a couple of days.

It occurs to me today that it might help solidify things in my brain if I attempt to handle a third use case. The existing post-response push sends data after the response has been sent, but before the response is closed (via a SPDY FIN).

In SPDY, it is possible to push the headers-only of the post-response pushes, close the response and then send the data. In other words, it is still possible to push data associated with a closed SPDY stream as long as the headers for the pushed data were sent prior to closing the stream.

I think I can get this work with this in the API:
var app = module.exports = express.createServer({
// ....
push: function(pusher) {
// Only push in response to the first request
if (pusher.streamID > 1) return;

pusher.push_file("public/stylesheets/style.css", "https://localhost:3000/stylesheets/style.css");

// push later
return [["public/one.html", "https://localhost:3000/one.html"],
["public/two.html", "https://localhost:3000/two.html"],
["public/three.html", "https://localhost:3000/three.html"]];

}
});
By returning those to-be-pushed resources, I can collate them in the Response#write method of node-spdy.

The code to do-so is quite ugly at this point, but I really am just spiking to see if this is even possible:
Response.prototype._write = function(data, encoding, fin) {
var push_later;
if (!this._written) {
this._flushHead();
// Resources to pushed post-response
push_later = this._push_stream();
}
// ...

var that = this,
deferred_streams = [];

// Send headers for each post-response server push stream, but DO
// NOT sent data yet
push_later.forEach(function(push_contents) {
var filename = push_contents[0]
, url = push_contents[1]
, data = fs.readFileSync(filename)
, push_stream = createPushStream(that.cframe, that.c, url);

push_stream._flushHead();
push_stream._written = true;
deferred_streams.push([push_stream, data]);
});

// Write the data frame
// ...

// Push any post-response push streams
deferred_streams.forEach(function(stream_and_data) {
var stream = stream_and_data[0]
, data = stream_and_data[1];

stream.write(data);
stream.end();
});
};
That's long (and I am not even showing the whole thing). Long and ugly. And I am reaching deep into the bowels of the PushStream class from Response. But... it does seem to work (according to Chrome's SPDY tab in about:net-internals):
#####
# The original request
t=1309751464484 [st= 0] SPDY_SESSION_SYN_STREAM
--> flags = 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
host: localhost:3000
method: GET
scheme: https
url: /
user-agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.797.0 Safari/535.1
version: HTTP/1.1
--> id = 1

#####
# Reply (Headers only, no data)
t=1309751464545 [st= 61] SPDY_SESSION_SYN_REPLY
--> flags = 0
--> connection: keep-alive
content-length: 303
content-type: text/html
status: 200 OK
version: HTTP/1.1
x-powered-by: Express
--> id = 1

#####
# Push the CSS -- including data
t=1309751464546 [st= 62] SPDY_SESSION_PUSHED_SYN_STREAM
--> associated_stream = 1
--> flags = 2
--> status: 200
url: https://localhost:3000/stylesheets/style.css
version: http/1.1
--> id = 2
t=1309751464547 [st= 63] SPDY_SESSION_RECV_DATA
--> flags = 0
--> size = 110
--> stream_id = 2


#####
# Post-response push -- headers only
t=1309751464555 [st= 71] SPDY_SESSION_PUSHED_SYN_STREAM
--> associated_stream = 1
--> flags = 2
--> content-type: text/html
status: 200
url: https://localhost:3000/one.html
version: http/1.1
--> id = 4
t=1309751464559 [st= 75] SPDY_SESSION_PUSHED_SYN_STREAM
--> associated_stream = 1
--> flags = 2
--> content-type: text/html
status: 200
url: https://localhost:3000/two.html
version: http/1.1
--> id = 6
t=1309751464560 [st= 76] SPDY_SESSION_PUSHED_SYN_STREAM
--> associated_stream = 1
--> flags = 2
--> content-type: text/html
status: 200
url: https://localhost:3000/three.html
version: http/1.1
--> id = 8

#####
# Response Data (stream ID is 1)
t=1309751464560 [st= 76] SPDY_SESSION_RECV_DATA
--> flags = 0
--> size = 303
--> stream_id = 1

#####
# Close the response stream
t=1309751464560 [st= 76] SPDY_SESSION_RECV_DATA
--> flags = 0
--> size = 0
--> stream_id = 1

####
# Push post-response data (after stream is closed)
t=1309751464564 [st= 80] SPDY_SESSION_RECV_DATA
--> flags = 0
--> size = 208
--> stream_id = 4

t=1309751464564 [st= 80] SPDY_SESSION_RECV_DATA
--> flags = 0
--> size = 208
--> stream_id = 6

t=1309751464565 [st= 81] SPDY_SESSION_RECV_DATA
--> flags = 0
--> size = 208
--> stream_id = 8

#####
# Everything is cool with the server push!
t=1309751464570 [st= 86] SPDY_STREAM_ADOPTED_PUSH_STREAM
There are no RST_STREAMs to be seen. The headers for the post-response push items are sent prior to the response. And the actual post-response push is sent after the response is sent and closed.

Much clean-up remains, but this was definitely interesting to see in action.


Day #64

No comments:

Post a Comment