Skip to content

Commit fa081f6

Browse files
committed
Remove pass thru behavior
* Require calling CopyTo to copy content to response stream * Use PagedByteBuffer to buffer content in memory
1 parent 692569d commit fa081f6

14 files changed

+872
-359
lines changed

src/Http/WebUtilities/src/FileBufferingWriteStream.cs

Lines changed: 85 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Buffers;
6+
using System.Diagnostics;
67
using System.IO;
78
using System.Threading;
89
using System.Threading.Tasks;
@@ -19,30 +20,23 @@ namespace Microsoft.AspNetCore.WebUtilities
1920
/// a temporary file on disk.
2021
/// </para>
2122
/// <para>
22-
/// The <see cref="FileBufferingWriteStream"/> performs opportunistic writes to the wrapping stream
23-
/// when asychronous operation such as <see cref="WriteAsync(byte[], int, int, CancellationToken)"/> or <see cref="FlushAsync(CancellationToken)"/>
24-
/// are performed.
23+
/// Consumers of this API can invoke <see cref="M:Stream.CopyToAsync" /> to copy the results to the HTTP Response Stream.
2524
/// </para>
2625
/// </summary>
2726
public sealed class FileBufferingWriteStream : Stream
2827
{
29-
private const int MaxRentedBufferSize = 1024 * 1024; // 1MB
30-
private const int DefaultMemoryThreshold = 30 * 1024; // 30k
28+
private const int DefaultMemoryThreshold = 32 * 1024; // 32k
3129

32-
private readonly Stream _writeStream;
3330
private readonly int _memoryThreshold;
3431
private readonly long? _bufferLimit;
3532
private readonly Func<string> _tempFileDirectoryAccessor;
36-
private readonly ArrayPool<byte> _bytePool;
37-
private readonly byte[] _rentedBuffer;
3833

3934
/// <summary>
4035
/// Initializes a new instance of <see cref="FileBufferingWriteStream"/>.
4136
/// </summary>
42-
/// <param name="writeStream">The <see cref="Stream"/> to write buffered contents to.</param>
4337
/// <param name="memoryThreshold">
4438
/// The maximum amount of memory in bytes to allocate before switching to a file on disk.
45-
/// Defaults to 30kb.
39+
/// Defaults to 32kb.
4640
/// </param>
4741
/// <param name="bufferLimit">
4842
/// The maximum amouont of bytes that the <see cref="FileBufferingWriteStream"/> is allowed to buffer.
@@ -52,24 +46,10 @@ public sealed class FileBufferingWriteStream : Stream
5246
/// uses the value returned by <see cref="Path.GetTempPath"/>.
5347
/// </param>
5448
public FileBufferingWriteStream(
55-
Stream writeStream,
5649
int memoryThreshold = DefaultMemoryThreshold,
5750
long? bufferLimit = null,
5851
Func<string> tempFileDirectoryAccessor = null)
59-
: this(writeStream, memoryThreshold, bufferLimit, tempFileDirectoryAccessor, ArrayPool<byte>.Shared)
6052
{
61-
62-
}
63-
64-
internal FileBufferingWriteStream(
65-
Stream writeStream,
66-
int memoryThreshold,
67-
long? bufferLimit,
68-
Func<string> tempFileDirectoryAccessor,
69-
ArrayPool<byte> bytePool)
70-
{
71-
_writeStream = writeStream ?? throw new ArgumentNullException(nameof(writeStream));
72-
7353
if (memoryThreshold < 0)
7454
{
7555
throw new ArgumentOutOfRangeException(nameof(memoryThreshold));
@@ -84,18 +64,7 @@ internal FileBufferingWriteStream(
8464
_memoryThreshold = memoryThreshold;
8565
_bufferLimit = bufferLimit;
8666
_tempFileDirectoryAccessor = tempFileDirectoryAccessor ?? AspNetCoreTempDirectory.TempDirectoryFactory;
87-
_bytePool = bytePool;
88-
89-
if (memoryThreshold < MaxRentedBufferSize)
90-
{
91-
_rentedBuffer = bytePool.Rent(memoryThreshold);
92-
MemoryStream = new MemoryStream(_rentedBuffer);
93-
MemoryStream.SetLength(0);
94-
}
95-
else
96-
{
97-
MemoryStream = new MemoryStream();
98-
}
67+
PagedByteBuffer = new PagedByteBuffer(ArrayPool<byte>.Shared);
9968
}
10069

10170
/// <inheritdoc />
@@ -117,9 +86,9 @@ public override long Position
11786
set => throw new NotSupportedException();
11887
}
11988

120-
internal long BufferedLength => MemoryStream.Length + (FileStream?.Length ?? 0);
89+
internal long BufferedLength => PagedByteBuffer.Length + (FileStream?.Length ?? 0);
12190

122-
internal MemoryStream MemoryStream { get; }
91+
internal PagedByteBuffer PagedByteBuffer { get; }
12392

12493
internal FileStream FileStream { get; private set; }
12594

@@ -144,22 +113,27 @@ public override void Write(byte[] buffer, int offset, int count)
144113

145114
if (_bufferLimit.HasValue && _bufferLimit - BufferedLength < count)
146115
{
147-
DiposeInternal();
116+
Dispose();
148117
throw new IOException("Buffer limit exceeded.");
149118
}
150119

151-
var availableMemory = _memoryThreshold - MemoryStream.Position;
152-
if (count <= availableMemory)
120+
// Allow buffering in memory if we're below the memory threshold once the current buffer is written.
121+
var allowMemoryBuffer = (_memoryThreshold - count) >= PagedByteBuffer.Length;
122+
if (allowMemoryBuffer)
153123
{
154124
// Buffer content in the MemoryStream if it has capacity.
155-
MemoryStream.Write(buffer, offset, count);
125+
PagedByteBuffer.Add(buffer, offset, count);
126+
Debug.Assert(PagedByteBuffer.Length <= _memoryThreshold);
156127
}
157128
else
158129
{
159130
// If the MemoryStream is incapable of accomodating the content to be written
160131
// spool to disk.
161132
EnsureFileStream();
162-
CopyContent(MemoryStream, FileStream);
133+
134+
// Spool memory content to disk and clear in memory buffers. We no longer need to hold on to it
135+
PagedByteBuffer.CopyTo(FileStream, clearBuffers: true);
136+
163137
FileStream.Write(buffer, offset, count);
164138
}
165139
}
@@ -170,36 +144,86 @@ public override async Task WriteAsync(byte[] buffer, int offset, int count, Canc
170144
ThrowArgumentException(buffer, offset, count);
171145
ThrowIfDisposed();
172146

173-
// If we have the opportunity to go async, write the buffered content to the response.
174-
await FlushAsync(cancellationToken);
175-
await _writeStream.WriteAsync(buffer, offset, count, cancellationToken);
147+
if (_bufferLimit.HasValue && _bufferLimit - BufferedLength < count)
148+
{
149+
Dispose();
150+
throw new IOException("Buffer limit exceeded.");
151+
}
152+
153+
// Allow buffering in memory if we're below the memory threshold once the current buffer is written.
154+
var allowMemoryBuffer = (_memoryThreshold - count) >= PagedByteBuffer.Length;
155+
if (allowMemoryBuffer)
156+
{
157+
// Buffer content in the MemoryStream if it has capacity.
158+
PagedByteBuffer.Add(buffer, offset, count);
159+
Debug.Assert(PagedByteBuffer.Length <= _memoryThreshold);
160+
}
161+
else
162+
{
163+
// If the MemoryStream is incapable of accomodating the content to be written
164+
// spool to disk.
165+
EnsureFileStream();
166+
167+
// Spool memory content to disk and clear in memory buffers. We no longer need to hold on to it
168+
await PagedByteBuffer.CopyToAsync(FileStream, clearBuffers: true, cancellationToken);
169+
170+
await FileStream.WriteAsync(buffer, offset, count, cancellationToken);
171+
}
172+
}
173+
174+
public override void Flush()
175+
{
176+
// Do nothing.
176177
}
177178

178179
/// <inheritdoc />
179180
public override void SetLength(long value) => throw new NotSupportedException();
180181

181-
/// <inheritdoc />
182-
// In the ordinary case, we expect this to throw if the target is the HttpResponse Body
183-
// and disallows synchronous writes. We do not need to optimize for this.
184-
public override void Flush()
182+
/// <summary>
183+
/// Copies buffered content to <paramref name="destination"/>.
184+
/// </summary>
185+
/// <param name="destination">The <see cref="Stream" /> to copy to.</param>
186+
/// <param name="bufferSize">The size of the buffer.</param>
187+
/// <param name="cancellationToken">The <see cref="CancellationToken" />.</param>
188+
/// <returns>A <see cref="Task" /> that represents the asynchronous copy operation.</returns>
189+
/// <remarks>
190+
/// Users of this API do not need to reset the <see cref="Position" /> of this instance, prior to copying content.
191+
/// </remarks>
192+
public override async Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken)
185193
{
194+
// When not null, FileStream always has "older" spooled content. The PagedByteBuffer always has "newer"
195+
// unspooled content. Copy the FileStream content first when available.
186196
if (FileStream != null)
187197
{
188-
CopyContent(FileStream, _writeStream);
198+
FileStream.Position = 0;
199+
await FileStream.CopyToAsync(destination, bufferSize, cancellationToken);
189200
}
190201

191-
CopyContent(MemoryStream, _writeStream);
202+
// Copy memory content, but do not clear the buffers. We want multiple invocations of CopyTo \ CopyToAsync
203+
// on this instance to behave the same.
204+
await PagedByteBuffer.CopyToAsync(destination, clearBuffers: false, cancellationToken);
192205
}
193206

194-
/// <inheritdoc />
195-
public override async Task FlushAsync(CancellationToken cancellationToken)
207+
/// <summary>
208+
/// Copies buffered content to <paramref name="destination"/>.
209+
/// </summary>
210+
/// <param name="destination">The <see cref="Stream" /> to copy to.</param>
211+
/// <param name="bufferSize">The size of the buffer.</param>
212+
/// <remarks>
213+
/// Users of this API do not need to reset the <see cref="Position" /> of this instance, prior to copying content.
214+
/// </remarks>
215+
public override void CopyTo(Stream destination, int bufferSize)
196216
{
217+
// See comments under CopyToAsync for an explanation for the order of execution.
197218
if (FileStream != null)
198219
{
199-
await CopyContentAsync(FileStream, _writeStream, cancellationToken);
220+
FileStream.Position = 0;
221+
FileStream.CopyTo(destination, bufferSize);
200222
}
201223

202-
await CopyContentAsync(MemoryStream, _writeStream, cancellationToken);
224+
// Copy memory content, but do not clear the buffers. We want multiple invocations of CopyTo \ CopyToAsync
225+
// on this instance to behave the same.
226+
PagedByteBuffer.CopyTo(destination, clearBuffers: false);
203227
}
204228

205229
/// <inheritdoc />
@@ -208,39 +232,21 @@ protected override void Dispose(bool disposing)
208232
if (!Disposed)
209233
{
210234
Disposed = true;
211-
Flush();
212235

213-
DiposeInternal();
236+
PagedByteBuffer.Dispose();
237+
FileStream?.Dispose();
214238
}
215239
}
216240

217-
private void DiposeInternal()
218-
{
219-
Disposed = true;
220-
_bytePool.Return(_rentedBuffer);
221-
MemoryStream.Dispose();
222-
FileStream?.Dispose();
223-
}
224-
225241
/// <inheritdoc />
226242
public override async ValueTask DisposeAsync()
227243
{
228244
if (!Disposed)
229245
{
230246
Disposed = true;
231-
try
232-
{
233-
await FlushAsync();
234-
}
235-
finally
236-
{
237-
if (_rentedBuffer != null)
238-
{
239-
_bytePool.Return(_rentedBuffer);
240-
}
241-
await MemoryStream.DisposeAsync();
242-
await (FileStream?.DisposeAsync() ?? default);
243-
}
247+
248+
PagedByteBuffer.Dispose();
249+
await (FileStream?.DisposeAsync() ?? default);
244250
}
245251
}
246252

@@ -290,19 +296,5 @@ private static void ThrowArgumentException(byte[] buffer, int offset, int count)
290296
throw new ArgumentOutOfRangeException(nameof(offset));
291297
}
292298
}
293-
294-
private static void CopyContent(Stream source, Stream destination)
295-
{
296-
source.Position = 0;
297-
source.CopyTo(destination);
298-
source.SetLength(0);
299-
}
300-
301-
private static async Task CopyContentAsync(Stream source, Stream destination, CancellationToken cancellationToken)
302-
{
303-
source.Position = 0;
304-
await source.CopyToAsync(destination, cancellationToken);
305-
source.SetLength(0);
306-
}
307299
}
308300
}

0 commit comments

Comments
 (0)