535 lines
21 KiB
C#
535 lines
21 KiB
C#
// Copyright (c) .NET Foundation. All rights reserved.
|
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
|
|
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.IO.Pipelines;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.AspNetCore.Server.Kestrel;
|
|
using Microsoft.AspNetCore.Server.Kestrel.Internal;
|
|
using Microsoft.AspNetCore.Server.Kestrel.Internal.Http;
|
|
using Microsoft.AspNetCore.Server.KestrelTests.TestHelpers;
|
|
using Microsoft.AspNetCore.Testing;
|
|
using Xunit;
|
|
|
|
namespace Microsoft.AspNetCore.Server.KestrelTests
|
|
{
|
|
public class SocketOutputTests : IDisposable
|
|
{
|
|
private readonly PipeFactory _pipeFactory;
|
|
private readonly MockLibuv _mockLibuv;
|
|
private readonly KestrelThread _kestrelThread;
|
|
|
|
public static TheoryData<long?> MaxResponseBufferSizeData => new TheoryData<long?>
|
|
{
|
|
new KestrelServerOptions().Limits.MaxResponseBufferSize, 0, 1024, 1024 * 1024, null
|
|
};
|
|
|
|
public static TheoryData<int> PositiveMaxResponseBufferSizeData => new TheoryData<int>
|
|
{
|
|
(int)new KestrelServerOptions().Limits.MaxResponseBufferSize, 1024, (1024 * 1024) + 1
|
|
};
|
|
|
|
public SocketOutputTests()
|
|
{
|
|
_pipeFactory = new PipeFactory();
|
|
_mockLibuv = new MockLibuv();
|
|
|
|
var kestrelEngine = new KestrelEngine(_mockLibuv, new TestServiceContext().TransportContext, new ListenOptions(0));
|
|
_kestrelThread = new KestrelThread(kestrelEngine, maxLoops: 1);
|
|
_kestrelThread.StartAsync().Wait();
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_kestrelThread.StopAsync(TimeSpan.FromSeconds(1)).Wait();
|
|
_pipeFactory.Dispose();
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(MaxResponseBufferSizeData))]
|
|
public async Task CanWrite1MB(long? maxResponseBufferSize)
|
|
{
|
|
// This test was added because when initially implementing write-behind buffering in
|
|
// SocketOutput, the write callback would never be invoked for writes larger than
|
|
// maxResponseBufferSize even after the write actually completed.
|
|
|
|
// ConnectionHandler will set MaximumSizeHigh/Low to zero when MaxResponseBufferSize is null.
|
|
// This is verified in PipeOptionsTests.OutputPipeOptionsConfiguredCorrectly.
|
|
var pipeOptions = new PipeOptions
|
|
{
|
|
ReaderScheduler = _kestrelThread,
|
|
MaximumSizeHigh = maxResponseBufferSize ?? 0,
|
|
MaximumSizeLow = maxResponseBufferSize ?? 0,
|
|
};
|
|
|
|
using (var socketOutput = CreateSocketOutput(pipeOptions))
|
|
{
|
|
// At least one run of this test should have a MaxResponseBufferSize < 1 MB.
|
|
var bufferSize = 1024 * 1024;
|
|
var buffer = new ArraySegment<byte>(new byte[bufferSize], 0, bufferSize);
|
|
|
|
// Act
|
|
var writeTask = socketOutput.WriteAsync(buffer, default(CancellationToken));
|
|
|
|
// Assert
|
|
await writeTask.TimeoutAfter(TimeSpan.FromSeconds(5));
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task NullMaxResponseBufferSizeAllowsUnlimitedBuffer()
|
|
{
|
|
var completeQueue = new ConcurrentQueue<Action<int>>();
|
|
|
|
// Arrange
|
|
_mockLibuv.OnWrite = (socket, buffers, triggerCompleted) =>
|
|
{
|
|
completeQueue.Enqueue(triggerCompleted);
|
|
return 0;
|
|
};
|
|
|
|
// ConnectionHandler will set MaximumSizeHigh/Low to zero when MaxResponseBufferSize is null.
|
|
// This is verified in PipeOptionsTests.OutputPipeOptionsConfiguredCorrectly.
|
|
var pipeOptions = new PipeOptions
|
|
{
|
|
ReaderScheduler = _kestrelThread,
|
|
MaximumSizeHigh = 0,
|
|
MaximumSizeLow = 0,
|
|
};
|
|
|
|
using (var socketOutput = CreateSocketOutput(pipeOptions))
|
|
{
|
|
// Don't want to allocate anything too huge for perf. This is at least larger than the default buffer.
|
|
var bufferSize = 1024 * 1024;
|
|
var buffer = new ArraySegment<byte>(new byte[bufferSize], 0, bufferSize);
|
|
|
|
// Act
|
|
var writeTask = socketOutput.WriteAsync(buffer, default(CancellationToken));
|
|
|
|
// Assert
|
|
await writeTask.TimeoutAfter(TimeSpan.FromSeconds(5));
|
|
|
|
// Cleanup
|
|
socketOutput.Dispose();
|
|
|
|
// Wait for all writes to complete so the completeQueue isn't modified during enumeration.
|
|
await _mockLibuv.OnPostTask;
|
|
|
|
foreach (var triggerCompleted in completeQueue)
|
|
{
|
|
await _kestrelThread.PostAsync(cb => cb(0), triggerCompleted);
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ZeroMaxResponseBufferSizeDisablesBuffering()
|
|
{
|
|
var completeQueue = new ConcurrentQueue<Action<int>>();
|
|
|
|
// Arrange
|
|
_mockLibuv.OnWrite = (socket, buffers, triggerCompleted) =>
|
|
{
|
|
completeQueue.Enqueue(triggerCompleted);
|
|
return 0;
|
|
};
|
|
|
|
// ConnectionHandler will set MaximumSizeHigh/Low to 1 when MaxResponseBufferSize is zero.
|
|
// This is verified in PipeOptionsTests.OutputPipeOptionsConfiguredCorrectly.
|
|
var pipeOptions = new PipeOptions
|
|
{
|
|
ReaderScheduler = _kestrelThread,
|
|
MaximumSizeHigh = 1,
|
|
MaximumSizeLow = 1,
|
|
};
|
|
|
|
using (var socketOutput = CreateSocketOutput(pipeOptions))
|
|
{
|
|
var bufferSize = 1;
|
|
var buffer = new ArraySegment<byte>(new byte[bufferSize], 0, bufferSize);
|
|
|
|
// Act
|
|
var writeTask = socketOutput.WriteAsync(buffer, default(CancellationToken));
|
|
|
|
// Assert
|
|
Assert.False(writeTask.IsCompleted);
|
|
|
|
// Act
|
|
await _mockLibuv.OnPostTask;
|
|
|
|
// Finishing the write should allow the task to complete.
|
|
Assert.True(completeQueue.TryDequeue(out var triggerNextCompleted));
|
|
await _kestrelThread.PostAsync(cb => cb(0), triggerNextCompleted);
|
|
|
|
// Assert
|
|
await writeTask.TimeoutAfter(TimeSpan.FromSeconds(5));
|
|
|
|
// Cleanup
|
|
socketOutput.Dispose();
|
|
|
|
// Wait for all writes to complete so the completeQueue isn't modified during enumeration.
|
|
await _mockLibuv.OnPostTask;
|
|
|
|
foreach (var triggerCompleted in completeQueue)
|
|
{
|
|
await _kestrelThread.PostAsync(cb => cb(0), triggerCompleted);
|
|
}
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(PositiveMaxResponseBufferSizeData))]
|
|
public async Task WritesDontCompleteImmediatelyWhenTooManyBytesAreAlreadyBuffered(int maxResponseBufferSize)
|
|
{
|
|
var completeQueue = new ConcurrentQueue<Action<int>>();
|
|
|
|
// Arrange
|
|
_mockLibuv.OnWrite = (socket, buffers, triggerCompleted) =>
|
|
{
|
|
completeQueue.Enqueue(triggerCompleted);
|
|
return 0;
|
|
};
|
|
|
|
var pipeOptions = new PipeOptions
|
|
{
|
|
ReaderScheduler = _kestrelThread,
|
|
MaximumSizeHigh = maxResponseBufferSize,
|
|
MaximumSizeLow = maxResponseBufferSize,
|
|
};
|
|
|
|
using (var socketOutput = CreateSocketOutput(pipeOptions))
|
|
{
|
|
var bufferSize = maxResponseBufferSize - 1;
|
|
var buffer = new ArraySegment<byte>(new byte[bufferSize], 0, bufferSize);
|
|
|
|
// Act
|
|
var writeTask1 = socketOutput.WriteAsync(buffer, default(CancellationToken));
|
|
|
|
// Assert
|
|
// The first write should pre-complete since it is <= _maxBytesPreCompleted.
|
|
Assert.Equal(TaskStatus.RanToCompletion, writeTask1.Status);
|
|
|
|
// Act
|
|
var writeTask2 = socketOutput.WriteAsync(buffer, default(CancellationToken));
|
|
await _mockLibuv.OnPostTask;
|
|
|
|
// Assert
|
|
// Too many bytes are already pre-completed for the second write to pre-complete.
|
|
Assert.False(writeTask2.IsCompleted);
|
|
|
|
// Act
|
|
Assert.True(completeQueue.TryDequeue(out var triggerNextCompleted));
|
|
await _kestrelThread.PostAsync(cb => cb(0), triggerNextCompleted);
|
|
|
|
// Finishing the first write should allow the second write to pre-complete.
|
|
await writeTask2.TimeoutAfter(TimeSpan.FromSeconds(5));
|
|
|
|
// Cleanup
|
|
socketOutput.Dispose();
|
|
|
|
// Wait for all writes to complete so the completeQueue isn't modified during enumeration.
|
|
await _mockLibuv.OnPostTask;
|
|
|
|
foreach (var triggerCompleted in completeQueue)
|
|
{
|
|
await _kestrelThread.PostAsync(cb => cb(0), triggerCompleted);
|
|
}
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(PositiveMaxResponseBufferSizeData))]
|
|
public async Task WritesDontCompleteImmediatelyWhenTooManyBytesIncludingNonImmediateAreAlreadyBuffered(int maxResponseBufferSize)
|
|
{
|
|
var completeQueue = new ConcurrentQueue<Action<int>>();
|
|
|
|
// Arrange
|
|
_mockLibuv.OnWrite = (socket, buffers, triggerCompleted) =>
|
|
{
|
|
completeQueue.Enqueue(triggerCompleted);
|
|
return 0;
|
|
};
|
|
|
|
var pipeOptions = new PipeOptions
|
|
{
|
|
ReaderScheduler = _kestrelThread,
|
|
MaximumSizeHigh = maxResponseBufferSize,
|
|
MaximumSizeLow = maxResponseBufferSize,
|
|
};
|
|
|
|
using (var socketOutput = CreateSocketOutput(pipeOptions))
|
|
{
|
|
var bufferSize = maxResponseBufferSize / 2;
|
|
var data = new byte[bufferSize];
|
|
var halfWriteBehindBuffer = new ArraySegment<byte>(data, 0, bufferSize);
|
|
|
|
// Act
|
|
var writeTask1 = socketOutput.WriteAsync(halfWriteBehindBuffer, default(CancellationToken));
|
|
|
|
// Assert
|
|
// The first write should pre-complete since it is <= _maxBytesPreCompleted.
|
|
Assert.Equal(TaskStatus.RanToCompletion, writeTask1.Status);
|
|
await _mockLibuv.OnPostTask;
|
|
Assert.NotEmpty(completeQueue);
|
|
|
|
// Add more bytes to the write-behind buffer to prevent the next write from
|
|
((ISocketOutput)socketOutput).Write((writableBuffer, state) =>
|
|
{
|
|
writableBuffer.Write(state);
|
|
},
|
|
halfWriteBehindBuffer);
|
|
|
|
// Act
|
|
var writeTask2 = socketOutput.WriteAsync(halfWriteBehindBuffer, default(CancellationToken));
|
|
Assert.False(writeTask2.IsCompleted);
|
|
|
|
var writeTask3 = socketOutput.WriteAsync(halfWriteBehindBuffer, default(CancellationToken));
|
|
Assert.False(writeTask3.IsCompleted);
|
|
|
|
// Drain the write queue
|
|
while (completeQueue.TryDequeue(out var triggerNextCompleted))
|
|
{
|
|
await _kestrelThread.PostAsync(cb => cb(0), triggerNextCompleted);
|
|
}
|
|
|
|
var timeout = TimeSpan.FromSeconds(5);
|
|
|
|
await writeTask2.TimeoutAfter(timeout);
|
|
await writeTask3.TimeoutAfter(timeout);
|
|
|
|
Assert.Empty(completeQueue);
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(PositiveMaxResponseBufferSizeData))]
|
|
public async Task FailedWriteCompletesOrCancelsAllPendingTasks(int maxResponseBufferSize)
|
|
{
|
|
var completeQueue = new ConcurrentQueue<Action<int>>();
|
|
|
|
// Arrange
|
|
_mockLibuv.OnWrite = (socket, buffers, triggerCompleted) =>
|
|
{
|
|
completeQueue.Enqueue(triggerCompleted);
|
|
return 0;
|
|
};
|
|
|
|
using (var mockConnection = new MockConnection())
|
|
{
|
|
var abortedSource = mockConnection.RequestAbortedSource;
|
|
|
|
var pipeOptions = new PipeOptions
|
|
{
|
|
ReaderScheduler = _kestrelThread,
|
|
MaximumSizeHigh = maxResponseBufferSize,
|
|
MaximumSizeLow = maxResponseBufferSize,
|
|
};
|
|
|
|
using (var socketOutput = CreateSocketOutput(pipeOptions, mockConnection))
|
|
{
|
|
var bufferSize = maxResponseBufferSize - 1;
|
|
|
|
var data = new byte[bufferSize];
|
|
var fullBuffer = new ArraySegment<byte>(data, 0, bufferSize);
|
|
|
|
// Act
|
|
var task1Success = socketOutput.WriteAsync(fullBuffer, cancellationToken: abortedSource.Token);
|
|
// task1 should complete successfully as < _maxBytesPreCompleted
|
|
|
|
// First task is completed and successful
|
|
Assert.True(task1Success.IsCompleted);
|
|
Assert.False(task1Success.IsCanceled);
|
|
Assert.False(task1Success.IsFaulted);
|
|
|
|
// following tasks should wait.
|
|
var task2Success = socketOutput.WriteAsync(fullBuffer, cancellationToken: default(CancellationToken));
|
|
var task3Canceled = socketOutput.WriteAsync(fullBuffer, cancellationToken: abortedSource.Token);
|
|
|
|
// Give time for tasks to percolate
|
|
await _mockLibuv.OnPostTask;
|
|
|
|
// Second task is not completed
|
|
Assert.False(task2Success.IsCompleted);
|
|
Assert.False(task2Success.IsCanceled);
|
|
Assert.False(task2Success.IsFaulted);
|
|
|
|
// Third task is not completed
|
|
Assert.False(task3Canceled.IsCompleted);
|
|
Assert.False(task3Canceled.IsCanceled);
|
|
Assert.False(task3Canceled.IsFaulted);
|
|
|
|
// Cause all writes to fail
|
|
while (completeQueue.TryDequeue(out var triggerNextCompleted))
|
|
{
|
|
await _kestrelThread.PostAsync(cb => cb(-1), triggerNextCompleted);
|
|
}
|
|
|
|
// Second task is now completed
|
|
await task2Success.TimeoutAfter(TimeSpan.FromSeconds(5));
|
|
|
|
// Third task is now canceled
|
|
// TODO: Cancellation isn't supported right now
|
|
// await Assert.ThrowsAsync<TaskCanceledException>(() => task3Canceled);
|
|
// Assert.True(task3Canceled.IsCanceled);
|
|
|
|
Assert.True(abortedSource.IsCancellationRequested);
|
|
}
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(PositiveMaxResponseBufferSizeData))]
|
|
public async Task WritesDontGetCompletedTooQuickly(int maxResponseBufferSize)
|
|
{
|
|
var completeQueue = new ConcurrentQueue<Action<int>>();
|
|
|
|
// Arrange
|
|
_mockLibuv.OnWrite = (socket, buffers, triggerCompleted) =>
|
|
{
|
|
completeQueue.Enqueue(triggerCompleted);
|
|
return 0;
|
|
};
|
|
|
|
var pipeOptions = new PipeOptions
|
|
{
|
|
ReaderScheduler = _kestrelThread,
|
|
MaximumSizeHigh = maxResponseBufferSize,
|
|
MaximumSizeLow = maxResponseBufferSize,
|
|
};
|
|
|
|
using (var socketOutput = CreateSocketOutput(pipeOptions))
|
|
{
|
|
var bufferSize = maxResponseBufferSize - 1;
|
|
var buffer = new ArraySegment<byte>(new byte[bufferSize], 0, bufferSize);
|
|
|
|
// Act (Pre-complete the maximum number of bytes in preparation for the rest of the test)
|
|
var writeTask1 = socketOutput.WriteAsync(buffer, default(CancellationToken));
|
|
|
|
// Assert
|
|
// The first write should pre-complete since it is < _maxBytesPreCompleted.
|
|
await _mockLibuv.OnPostTask;
|
|
Assert.Equal(TaskStatus.RanToCompletion, writeTask1.Status);
|
|
Assert.NotEmpty(completeQueue);
|
|
|
|
// Act
|
|
var writeTask2 = socketOutput.WriteAsync(buffer, default(CancellationToken));
|
|
var writeTask3 = socketOutput.WriteAsync(buffer, default(CancellationToken));
|
|
|
|
// Drain the write queue
|
|
while (completeQueue.TryDequeue(out var triggerNextCompleted))
|
|
{
|
|
await _kestrelThread.PostAsync(cb => cb(0), triggerNextCompleted);
|
|
}
|
|
|
|
var timeout = TimeSpan.FromSeconds(5);
|
|
|
|
// Assert
|
|
// Too many bytes are already pre-completed for the third but not the second write to pre-complete.
|
|
// https://github.com/aspnet/KestrelHttpServer/issues/356
|
|
await writeTask2.TimeoutAfter(timeout);
|
|
await writeTask3.TimeoutAfter(timeout);
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(MaxResponseBufferSizeData))]
|
|
public async Task WritesAreAggregated(long? maxResponseBufferSize)
|
|
{
|
|
var writeCalled = false;
|
|
var writeCount = 0;
|
|
|
|
_mockLibuv.OnWrite = (socket, buffers, triggerCompleted) =>
|
|
{
|
|
writeCount++;
|
|
triggerCompleted(0);
|
|
writeCalled = true;
|
|
return 0;
|
|
};
|
|
|
|
// ConnectionHandler will set MaximumSizeHigh/Low to zero when MaxResponseBufferSize is null.
|
|
// This is verified in PipeOptionsTests.OutputPipeOptionsConfiguredCorrectly.
|
|
var pipeOptions = new PipeOptions
|
|
{
|
|
ReaderScheduler = _kestrelThread,
|
|
MaximumSizeHigh = maxResponseBufferSize ?? 0,
|
|
MaximumSizeLow = maxResponseBufferSize ?? 0,
|
|
};
|
|
|
|
using (var socketOutput = CreateSocketOutput(pipeOptions))
|
|
{
|
|
_mockLibuv.KestrelThreadBlocker.Reset();
|
|
|
|
var buffer = new ArraySegment<byte>(new byte[1]);
|
|
|
|
// Two calls to WriteAsync trigger uv_write once if both calls
|
|
// are made before write is scheduled
|
|
var ignore = socketOutput.WriteAsync(buffer, CancellationToken.None);
|
|
ignore = socketOutput.WriteAsync(buffer, CancellationToken.None);
|
|
|
|
_mockLibuv.KestrelThreadBlocker.Set();
|
|
|
|
await _mockLibuv.OnPostTask;
|
|
|
|
Assert.True(writeCalled);
|
|
writeCalled = false;
|
|
|
|
// Write isn't called twice after the thread is unblocked
|
|
await _mockLibuv.OnPostTask;
|
|
|
|
Assert.False(writeCalled);
|
|
// One call to ScheduleWrite
|
|
Assert.Equal(1, _mockLibuv.PostCount);
|
|
// One call to uv_write
|
|
Assert.Equal(1, writeCount);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AllocCommitCanBeCalledAfterConnectionClose()
|
|
{
|
|
var pipeOptions = new PipeOptions
|
|
{
|
|
ReaderScheduler = _kestrelThread,
|
|
};
|
|
|
|
using (var connection = new MockConnection())
|
|
{
|
|
var socketOutput = CreateSocketOutput(pipeOptions, connection);
|
|
// Close SocketOutput
|
|
socketOutput.Dispose();
|
|
|
|
await _mockLibuv.OnPostTask;
|
|
|
|
Assert.Equal(TaskStatus.RanToCompletion, connection.SocketClosed.Status);
|
|
|
|
var called = false;
|
|
|
|
((ISocketOutput)socketOutput).Write<object>((buffer, state) =>
|
|
{
|
|
called = true;
|
|
},
|
|
null);
|
|
|
|
Assert.False(called);
|
|
}
|
|
}
|
|
|
|
private SocketOutputProducer CreateSocketOutput(PipeOptions pipeOptions, MockConnection connection = null)
|
|
{
|
|
var pipe = _pipeFactory.Create(pipeOptions);
|
|
var serviceContext = new TestServiceContext();
|
|
var trace = serviceContext.Log;
|
|
|
|
var frame = new Frame<object>(null, new FrameContext { ServiceContext = serviceContext });
|
|
|
|
var socket = new MockSocket(_mockLibuv, _kestrelThread.Loop.ThreadId, new TestKestrelTrace());
|
|
var socketOutput = new SocketOutputProducer(pipe.Writer, frame, "0", trace);
|
|
var consumer = new SocketOutputConsumer(pipe.Reader, _kestrelThread, socket, connection ?? new MockConnection(), "0", trace);
|
|
var ignore = consumer.StartWrites();
|
|
|
|
return socketOutput;
|
|
}
|
|
}
|
|
} |