Tuesday, June 28, 2011

Vows to Make SPDY Push Better

‹prev | My Chain | next›

I think that I want a better API for pushing SPDY data before and after responses than:
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");
}
});
Ultimately, I think I would like to be able to tell the SPDY server that, for a given resource, push this data before the response and that data after the response. I think I want that, but I am not sure. What I do know is that I made a bit of a mess of the Response#write method in node-spdy.

Uncertainty and messy code. Sounds like a good time to write some tests.

Node-spdy uses vows.js for its testing, so I start on a test/response-test.js file.

There is a bit of setup for a control frame that is required for createResponse(), so I make that setup the topic of my first batch of tests:
vows.describe('Pushing Additional Resources').addBatch({
'A response': {
topic: function() {
var cframe = spdy.createControlFrame(spdy.createZLib(), {
type: spdy.enums.SYN_STREAM,
streamID: 1,
priority: 0,
flags: 0
}, {
version: 'HTTP/1.1',
url: '/',
method: 'GET'
});

var connection = {
zlib: spdy.createZLib(),
write: function() {}
};

return spdy.createResponse(cframe, connection);
}
}
}).run();
For now, I do not want to test directly on that Response object. Rather, I want to verify that my push-stream callbacks are being called. Fortunately, vows makes it easy to verify that a callback is called with the this.callback construct. If this.callback is called, its arguments become the topic of the test:
vows.describe('Pushing Additional Resources').addBatch({
'A response': {
topic: function() {
// ...
return spdy.createResponse(cframe, connection);
},

'with a push_stream call': {
topic: function (response) {
var callback = this.callback;
response.setPush(callback);
response._write('foo');
}
}

}
}).run();
Finally, I can verify not only that my push-stream callbacks are called, but are called with the Response object itself:
vows.describe('Pushing Additional Resources').addBatch({
'A response': {
topic: function() {
// ...
return spdy.createResponse(cframe, connection);
},

'with a push_stream call': {
topic: function (response) {
var callback = this.callback;
response.setPush(callback);
response._write('foo');
},
'should push data on write': function (pusher) {
assert.equal(pusher, response);
}

}
}
}).run();
Unfortunately, when I run my assembled vows, I am greeted with:
➜ node-spdy git:(post-response-push) ✗ node test/response-test.js

node.js:183
throw e; // process.nextTick error, or 'error' event on first tick
^
TypeError: Cannot read property 'streamID' of undefined
at new <anonymous> (/home/cstrom/repos/node-spdy/lib/spdy/response.js:22:30)
...
That undefined bit of code from Response is the data attribute of the cframe that I created in my original topic.

Hrmm...

Ah, it seems that cframess and dframes, at least when downstream objects receive them, are not Buffers wrapped in an object. Rather they are just objects:
  this.emit(headers.c ? 'cframe' : 'dframe', {
headers: headers,
data: data
});
That simplifies my testing a bit, but now when I run my vows, I get:
➜  node-spdy git:(post-response-push) ✗ vows test/response-test.js


A response with a push_stream call
✗ should push data on write
» An unexpected error was caught: {"cframe":{"0":128,"1":2,"2":0,"3":1,"4":0,"5":0,"6":0,"7":50,"8":0,"9":0,"10":0,"11":1,"12":0,"13":0,"14":0,"15":0,"16":189,"17":0,"18":120,"19":187,"20":223,"21":162,"22":81,"23":178,"24":98,"25":96,"26":102,"27":96,"28":135,"29":114,"30":25,"31":56,"32":96,"33":170,"34":24,"35":152,
...
"An unexpected error was caught"? What does that mean? Did I throw that object somewhere deep in the bowels of node-spdy?

Googling this error, I come across this post from a very clever author. Reading through that post leads me to ask...

How can I be the only idiot making that mistake?!

OK, so callback needs to be called with two arguments. My push-streams only callback with a single argument.
var response = spdy.createResponse(cframe, connection);

vows.describe('Pushing Additional Resources').addBatch({
'A response': {
topic: response,

'with a push_stream call': {
topic: function (response) {
var callback = this.callback;
response.setPush(function(pusher) {callback(null, pusher)});
response._write();
},
'should push data on write': function (pusher) {
assert.equal(pusher, response);
}
}
}
}).export(module);
With that, I finally have my vows in working order:
➜  node-spdy git:(post-response-push) ✗ vows test/response-test.js --spec

♢ Pushing Additional Resources

function
A response with a push_stream call
✓ should push data on write

✓ OK » 1 honored (0.003s)
I call it a day there. I have made a good faith attempt at getting started on specs for server push in node-spdy. Tomorrow, I will build on this by adding tests for post-response push.

But, for now, I'm off to write SPDY Book.


Day #60

No comments:

Post a Comment