Posts Tagged ‘asp.net’
The Response.OutputStream floater
Today we had to recall a release due to a message like:
Not enough memory for operation.
The error occured while trying to emit a large (~800MB) zip file via ASP.NET. Even though I knew buffering was on, I figured that because we were writing directly to HttpResponse.OutputStream that we would somehow bypass the buffering enforced by HttpResponse.
Because we were flushing HttpResponse.OutputStream regularly during processing I thought we were okay. I didn’t realise that HttpResponse.OutputStream was bound by the same buffering policy.
My theory is as follows.
Response.OutputStream.Flush does nothing
Here’s what happens when we call HttpResponse.OutputStream.Flush:
[Pseudo Stacktrace] HttpWriter.Flush HttpResponseStream._writer.Flush HttpResponseStream.Flush HttpWriter._stream.Flush HttpWriter.OutputStream.Flush HttpResponse._httpWriter.OutputStream.Flush HttpResponse.OutputStream.Flush
And HttpWriter.Flush looks like this:
// HttpWriter public override void Flush() { }
That’s right: it’s empty. Flushing is completely ignored. I guess this makes sense, HttpResponse maintains full control of its underlying stream.
Flushing depends entirely on HttpResponse.BufferOutput
Here’s what happens when we call HttpResponse.OutputStream.Write:
[Pseudo Stacktrace] HttpWriter.WriteFromStream HttpResponseStream._writer.WriteFromStream HttpResponseStream.Write HttpWriter._stream.Write HttpWriter.OutputStream.Write HttpResponse._httpWriter.OutputStream.Write HttpResponse.OutputStream.Write
And HttpWriter.WriteFromStream looks like:
// HttpWriter
internal void WriteFromStream(byte[] data, int offset, int size)
{
if (this._charBufferLength != this._charBufferFree)
{
this.FlushCharBuffer(true);
}
this.BufferData(data, offset, size, true);
if (!this._responseBufferingOn)
{
this._response.Flush();
}
}
Therefore, flushing depends on the HttpWriter._responseBufferingOn:
[Pseudo stacktrace] HttpResponse.BufferOutput HttpWriter._response.BufferOutput HttpWriter._responseBufferingOn
Provided we have HttpResponse.Buffer set to false, we should get flushing every time we write to Response.OutputStream.
That was the theory anyway. In practice we experienced high memory consumption still. This seems very weird to me. By turning buffering off — we should be flushing the response at every write as shown by the last line of HttpWriter.WriteFromStream above. [TBD: Investigate this properly].
Solution
In the end, though, we decided to implement this by explictly calling HttpResponse.Flush whenever Response.OutputStream.Flush is invoked. But how? Passing an HttpResponse into our writer API is just pollution.
Our zip files API are already doing everything correctly — it just happens that one Stream implementation is not working as expected.
We needed to modify what happens when call Stream.Flush. To do this without breaking our abstraction, we created a Stream decorator type.
This type takes a Stream, and an HttpResponse to decorate Stream operations. Mostly this decorator delegates all of its Stream operations, but some are decorated — like Flush. It is here we are invoking HttpResponse.Flush.
The nice thing about this design is that no changes at all were required within the zip API. It is written in terms of an abstraction — Stream. Our decorator is completely transparent, it is just another Stream.
Another option may be to implement an ObservableStream using decoration. This allows a client (our view) to observe a Stream object and respond to changes in state. In this case, we can raise a Flush event, which our view can respond to by flushing the HttpResponse for the request. This was feels cleaner somehow — though we sacrifice some testability.
Related patterns
Are we decorating, or proxying? Well, according to the GoF, we are decorating because we’re adding responsibilities rather than controlling access to an object.
We are decorating a Stream by adding the responsibility of flushing an HTTP response. Or, in the second case, adding the responsibility of notifying observers of state changes on a Stream.
Incidentally
Download dialog is misleading
You may think your download doesn’t start until you confirm — actually it’s already started by this point. You can see this for yourself if you inspect your HTTP connections. Interestingly, Chrome doesn’t seem to offer a dialog at all.
This is because this is just an HTTP response like any other. There is no special callback in place. Once response has been started, your browser is reading it. Cancelling will disconnect though, hence it’s important to check the client is still connected while emitting large files.
[TBD: With that in mind then, what happens to the data if we continue writing but the client has disconnected? Do the bytes just fall out the end? Or doesan error result?]
Response.Buffer == Response.BufferOutput
You can optionally use HttpResponse.Buffer, or HttpResponse.BufferOutput: they do the same thing:
// HttpResponse public bool Buffer { get { return this.BufferOutput; } set { this.BufferOutput = value; } }
[UPDATE, 2009-10-30] We have encountered an issue which has required us to set buffering on. It seems that we get poorer throughput with buffering off for some reason. There has got to be some reason for this — something in HttpResponse.Flush(Boolean finalFlush) is optimizing.
References
Configuring routes in ASP.NET MVC
Routes can be configured as follows:
routes.Add( new Route( "files/{id}", // URL pattern new RouteValueDictionary // Defaults { { "controller" , "file"}, // Default controller is file { "action" , "index"}, { "id" , null} }, new MvcRouteHandler() ) );
Which means an URL like:
/files/12345
returns the file with id equal to 12345.