Add ListenerPrimaryTests
This commit is contained in:
parent
eee9520ffd
commit
ecc8a0088d
|
|
@ -0,0 +1,229 @@
|
|||
// 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.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Hosting.Server;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Server.Kestrel;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Internal;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Internal.Http;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Internal.Networking;
|
||||
using Microsoft.AspNetCore.Testing;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.KestrelTests
|
||||
{
|
||||
public class ListenerPrimaryTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ConnectionsGetRoundRobinedToSecondaryListeners()
|
||||
{
|
||||
var libuv = new Libuv();
|
||||
|
||||
var serviceContextPrimary = new TestServiceContext
|
||||
{
|
||||
FrameFactory = context =>
|
||||
{
|
||||
return new Frame<DefaultHttpContext>(new TestApplication(c =>
|
||||
{
|
||||
return c.Response.WriteAsync("Primary");
|
||||
}), context);
|
||||
}
|
||||
};
|
||||
|
||||
var serviceContextSecondary = new ServiceContext
|
||||
{
|
||||
Log = serviceContextPrimary.Log,
|
||||
AppLifetime = serviceContextPrimary.AppLifetime,
|
||||
DateHeaderValueManager = serviceContextPrimary.DateHeaderValueManager,
|
||||
ServerOptions = serviceContextPrimary.ServerOptions,
|
||||
ThreadPool = serviceContextPrimary.ThreadPool,
|
||||
FrameFactory = context =>
|
||||
{
|
||||
return new Frame<DefaultHttpContext>(new TestApplication(c =>
|
||||
{
|
||||
return c.Response.WriteAsync("Secondary"); ;
|
||||
}), context);
|
||||
}
|
||||
};
|
||||
|
||||
using (var kestrelEngine = new KestrelEngine(libuv, serviceContextPrimary))
|
||||
{
|
||||
var address = ServerAddress.FromUrl("http://127.0.0.1:0/");
|
||||
var pipeName = (libuv.IsWindows ? @"\\.\pipe\kestrel_" : "/tmp/kestrel_") + Guid.NewGuid().ToString("n");
|
||||
|
||||
// Start primary listener
|
||||
var kestrelThreadPrimary = new KestrelThread(kestrelEngine);
|
||||
await kestrelThreadPrimary.StartAsync();
|
||||
var listenerPrimary = new TcpListenerPrimary(serviceContextPrimary);
|
||||
await listenerPrimary.StartAsync(pipeName, address, kestrelThreadPrimary);
|
||||
|
||||
// Until a secondary listener is added, TCP connections get dispatched directly
|
||||
Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address.ToString()));
|
||||
Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address.ToString()));
|
||||
|
||||
// Add secondary listener
|
||||
var kestrelThreadSecondary = new KestrelThread(kestrelEngine);
|
||||
await kestrelThreadSecondary.StartAsync();
|
||||
var listenerSecondary = new TcpListenerSecondary(serviceContextSecondary);
|
||||
await listenerSecondary.StartAsync(pipeName, address, kestrelThreadSecondary);
|
||||
|
||||
// Once a secondary listener is added, TCP connections start getting dispatched to it
|
||||
Assert.Equal("Secondary", await HttpClientSlim.GetStringAsync(address.ToString()));
|
||||
|
||||
// TCP connections will still get round-robined to the primary listener
|
||||
Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address.ToString()));
|
||||
Assert.Equal("Secondary", await HttpClientSlim.GetStringAsync(address.ToString()));
|
||||
Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address.ToString()));
|
||||
|
||||
await listenerSecondary.DisposeAsync();
|
||||
kestrelThreadSecondary.Stop(TimeSpan.FromSeconds(1));
|
||||
|
||||
await listenerPrimary.DisposeAsync();
|
||||
kestrelThreadPrimary.Stop(TimeSpan.FromSeconds(1));
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/aspnet/KestrelHttpServer/issues/1182
|
||||
[Fact]
|
||||
public async Task NonListenerPipeConnectionsAreLoggedAndIgnored()
|
||||
{
|
||||
var libuv = new Libuv();
|
||||
|
||||
var primaryTrace = new TestKestrelTrace();
|
||||
|
||||
var serviceContextPrimary = new TestServiceContext
|
||||
{
|
||||
Log = primaryTrace,
|
||||
FrameFactory = context =>
|
||||
{
|
||||
return new Frame<DefaultHttpContext>(new TestApplication(c =>
|
||||
{
|
||||
return c.Response.WriteAsync("Primary");
|
||||
}), context);
|
||||
}
|
||||
};
|
||||
|
||||
var serviceContextSecondary = new ServiceContext
|
||||
{
|
||||
Log = new TestKestrelTrace(),
|
||||
AppLifetime = serviceContextPrimary.AppLifetime,
|
||||
DateHeaderValueManager = serviceContextPrimary.DateHeaderValueManager,
|
||||
ServerOptions = serviceContextPrimary.ServerOptions,
|
||||
ThreadPool = serviceContextPrimary.ThreadPool,
|
||||
FrameFactory = context =>
|
||||
{
|
||||
return new Frame<DefaultHttpContext>(new TestApplication(c =>
|
||||
{
|
||||
return c.Response.WriteAsync("Secondary"); ;
|
||||
}), context);
|
||||
}
|
||||
};
|
||||
|
||||
using (var kestrelEngine = new KestrelEngine(libuv, serviceContextPrimary))
|
||||
{
|
||||
var address = ServerAddress.FromUrl("http://127.0.0.1:0/");
|
||||
var pipeName = (libuv.IsWindows ? @"\\.\pipe\kestrel_" : "/tmp/kestrel_") + Guid.NewGuid().ToString("n");
|
||||
|
||||
// Start primary listener
|
||||
var kestrelThreadPrimary = new KestrelThread(kestrelEngine);
|
||||
await kestrelThreadPrimary.StartAsync();
|
||||
var listenerPrimary = new TcpListenerPrimary(serviceContextPrimary);
|
||||
await listenerPrimary.StartAsync(pipeName, address, kestrelThreadPrimary);
|
||||
|
||||
// Add secondary listener
|
||||
var kestrelThreadSecondary = new KestrelThread(kestrelEngine);
|
||||
await kestrelThreadSecondary.StartAsync();
|
||||
var listenerSecondary = new TcpListenerSecondary(serviceContextSecondary);
|
||||
await listenerSecondary.StartAsync(pipeName, address, kestrelThreadSecondary);
|
||||
|
||||
// TCP Connections get round-robined
|
||||
Assert.Equal("Secondary", await HttpClientSlim.GetStringAsync(address.ToString()));
|
||||
Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address.ToString()));
|
||||
|
||||
// Create a pipe connection and keep it open without sending any data
|
||||
var connectTcs = new TaskCompletionSource<object>();
|
||||
var connectionTrace = new TestKestrelTrace();
|
||||
var pipe = new UvPipeHandle(connectionTrace);
|
||||
|
||||
kestrelThreadPrimary.Post(_ =>
|
||||
{
|
||||
var connectReq = new UvConnectRequest(connectionTrace);
|
||||
|
||||
pipe.Init(kestrelThreadPrimary.Loop, kestrelThreadPrimary.QueueCloseHandle);
|
||||
connectReq.Init(kestrelThreadPrimary.Loop);
|
||||
|
||||
connectReq.Connect(
|
||||
pipe,
|
||||
pipeName,
|
||||
(req, status, ex, __) =>
|
||||
{
|
||||
req.Dispose();
|
||||
|
||||
if (ex != null)
|
||||
{
|
||||
connectTcs.SetException(ex);
|
||||
}
|
||||
else
|
||||
{
|
||||
connectTcs.SetResult(null);
|
||||
}
|
||||
},
|
||||
null);
|
||||
}, null);
|
||||
|
||||
await connectTcs.Task;
|
||||
|
||||
// TCP connections will still get round-robined between only the two listeners
|
||||
Assert.Equal("Secondary", await HttpClientSlim.GetStringAsync(address.ToString()));
|
||||
Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address.ToString()));
|
||||
Assert.Equal("Secondary", await HttpClientSlim.GetStringAsync(address.ToString()));
|
||||
|
||||
await kestrelThreadPrimary.PostAsync(_ => pipe.Dispose(), null);
|
||||
|
||||
// Same for after the non-listener pipe connection is closed
|
||||
Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address.ToString()));
|
||||
Assert.Equal("Secondary", await HttpClientSlim.GetStringAsync(address.ToString()));
|
||||
Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address.ToString()));
|
||||
|
||||
await listenerSecondary.DisposeAsync();
|
||||
kestrelThreadSecondary.Stop(TimeSpan.FromSeconds(1));
|
||||
|
||||
await listenerPrimary.DisposeAsync();
|
||||
kestrelThreadPrimary.Stop(TimeSpan.FromSeconds(1));
|
||||
}
|
||||
|
||||
Assert.Equal(1, primaryTrace.Logger.TotalErrorsLogged);
|
||||
var errorMessage = primaryTrace.Logger.Messages.First(m => m.LogLevel == LogLevel.Error);
|
||||
Assert.Contains("EOF", errorMessage.Exception.ToString());
|
||||
}
|
||||
|
||||
private class TestApplication : IHttpApplication<DefaultHttpContext>
|
||||
{
|
||||
private readonly Func<DefaultHttpContext, Task> _app;
|
||||
|
||||
public TestApplication(Func<DefaultHttpContext, Task> app)
|
||||
{
|
||||
_app = app;
|
||||
}
|
||||
|
||||
public DefaultHttpContext CreateContext(IFeatureCollection contextFeatures)
|
||||
{
|
||||
return new DefaultHttpContext(contextFeatures);
|
||||
}
|
||||
|
||||
public Task ProcessRequestAsync(DefaultHttpContext context)
|
||||
{
|
||||
return _app(context);
|
||||
}
|
||||
|
||||
public void DisposeContext(DefaultHttpContext context, Exception exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
// 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.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Security;
|
||||
using System.Net.Sockets;
|
||||
using System.Security.Authentication;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.Testing
|
||||
{
|
||||
// Lightweight version of HttpClient implemented using Socket and SslStream
|
||||
public static class HttpClientSlim
|
||||
{
|
||||
public static Task<string> GetStringAsync(string requestUri, bool validateCertificate = true)
|
||||
=> GetStringAsync(new Uri(requestUri), validateCertificate);
|
||||
|
||||
public static async Task<string> GetStringAsync(Uri requestUri, bool validateCertificate = true)
|
||||
{
|
||||
using (var stream = await GetStream(requestUri, validateCertificate))
|
||||
{
|
||||
using (var writer = new StreamWriter(stream, Encoding.ASCII, bufferSize: 1024, leaveOpen: true))
|
||||
{
|
||||
await writer.WriteAsync($"GET {requestUri.PathAndQuery} HTTP/1.0\r\n");
|
||||
await writer.WriteAsync($"Host: {requestUri.Authority}\r\n");
|
||||
await writer.WriteAsync("\r\n");
|
||||
}
|
||||
|
||||
return await ReadResponse(stream);
|
||||
}
|
||||
}
|
||||
|
||||
public static Task<string> PostAsync(string requestUri, HttpContent content, bool validateCertificate = true)
|
||||
=> PostAsync(new Uri(requestUri), content, validateCertificate);
|
||||
|
||||
public static async Task<string> PostAsync(Uri requestUri, HttpContent content, bool validateCertificate = true)
|
||||
{
|
||||
using (var stream = await GetStream(requestUri, validateCertificate))
|
||||
{
|
||||
using (var writer = new StreamWriter(stream, Encoding.ASCII, bufferSize: 1024, leaveOpen: true))
|
||||
{
|
||||
await writer.WriteAsync($"POST {requestUri.PathAndQuery} HTTP/1.0\r\n");
|
||||
await writer.WriteAsync($"Host: {requestUri.Authority}\r\n");
|
||||
await writer.WriteAsync($"Content-Type: {content.Headers.ContentType}\r\n");
|
||||
await writer.WriteAsync($"Content-Length: {content.Headers.ContentLength}\r\n");
|
||||
await writer.WriteAsync("\r\n");
|
||||
}
|
||||
|
||||
await content.CopyToAsync(stream);
|
||||
|
||||
return await ReadResponse(stream);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<string> ReadResponse(Stream stream)
|
||||
{
|
||||
using (var reader = new StreamReader(stream, Encoding.ASCII, detectEncodingFromByteOrderMarks: true,
|
||||
bufferSize: 1024, leaveOpen: true))
|
||||
{
|
||||
var response = await reader.ReadToEndAsync();
|
||||
|
||||
var status = GetStatus(response);
|
||||
new HttpResponseMessage(status).EnsureSuccessStatusCode();
|
||||
|
||||
var body = response.Substring(response.IndexOf("\r\n\r\n") + 4);
|
||||
return body;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static HttpStatusCode GetStatus(string response)
|
||||
{
|
||||
var statusStart = response.IndexOf(' ') + 1;
|
||||
var statusEnd = response.IndexOf(' ', statusStart) - 1;
|
||||
var statusLength = statusEnd - statusStart + 1;
|
||||
return (HttpStatusCode)int.Parse(response.Substring(statusStart, statusLength));
|
||||
}
|
||||
|
||||
private static async Task<Stream> GetStream(Uri requestUri, bool validateCertificate)
|
||||
{
|
||||
var socket = await GetSocket(requestUri);
|
||||
Stream stream = new NetworkStream(socket, ownsSocket: true);
|
||||
|
||||
if (requestUri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var sslStream = new SslStream(stream, leaveInnerStreamOpen: false, userCertificateValidationCallback:
|
||||
validateCertificate ? null : (RemoteCertificateValidationCallback)((a, b, c, d) => true));
|
||||
await sslStream.AuthenticateAsClientAsync(requestUri.Host, clientCertificates: null,
|
||||
enabledSslProtocols: SslProtocols.Tls11 | SslProtocols.Tls12,
|
||||
checkCertificateRevocation: validateCertificate);
|
||||
return sslStream;
|
||||
}
|
||||
else
|
||||
{
|
||||
return stream;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<Socket> GetSocket(Uri requestUri)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<Socket>();
|
||||
|
||||
var socketArgs = new SocketAsyncEventArgs();
|
||||
socketArgs.RemoteEndPoint = new DnsEndPoint(requestUri.DnsSafeHost, requestUri.Port);
|
||||
socketArgs.Completed += (s, e) => tcs.TrySetResult(e.ConnectSocket);
|
||||
|
||||
// Must use static ConnectAsync(), since instance Connect() does not support DNS names on OSX/Linux.
|
||||
if (Socket.ConnectAsync(SocketType.Stream, ProtocolType.Tcp, socketArgs))
|
||||
{
|
||||
await tcs.Task;
|
||||
}
|
||||
|
||||
var socket = socketArgs.ConnectSocket;
|
||||
|
||||
if (socket == null)
|
||||
{
|
||||
throw new SocketException((int)socketArgs.SocketError);
|
||||
}
|
||||
else
|
||||
{
|
||||
return socket;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,8 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Server.Kestrel;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Internal;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
|
|
@ -13,9 +14,13 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
|
|||
// Application errors are logged using 13 as the eventId.
|
||||
private const int ApplicationErrorEventId = 13;
|
||||
|
||||
public int TotalErrorsLogged { get; set; }
|
||||
public List<LogMessage> Messages { get; } = new List<LogMessage>();
|
||||
|
||||
public int ApplicationErrorsLogged { get; set; }
|
||||
public int TotalErrorsLogged => Messages.Count(message => message.LogLevel == LogLevel.Error);
|
||||
|
||||
public int CriticalErrorsLogged => Messages.Count(message => message.LogLevel == LogLevel.Critical);
|
||||
|
||||
public int ApplicationErrorsLogged => Messages.Count(message => message.EventId.Id == ApplicationErrorEventId);
|
||||
|
||||
public IDisposable BeginScope<TState>(TState state)
|
||||
{
|
||||
|
|
@ -33,15 +38,14 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
|
|||
Console.WriteLine($"Log {logLevel}[{eventId}]: {formatter(state, exception)} {exception?.Message}");
|
||||
#endif
|
||||
|
||||
if (eventId.Id == ApplicationErrorEventId)
|
||||
{
|
||||
ApplicationErrorsLogged++;
|
||||
}
|
||||
Messages.Add(new LogMessage { LogLevel = logLevel, EventId = eventId, Exception = exception });
|
||||
}
|
||||
|
||||
if (logLevel == LogLevel.Error)
|
||||
{
|
||||
TotalErrorsLogged++;
|
||||
}
|
||||
public class LogMessage
|
||||
{
|
||||
public LogLevel LogLevel { get; set; }
|
||||
public EventId EventId { get; set; }
|
||||
public Exception Exception { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
// 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 Microsoft.AspNetCore.Server.Kestrel.Internal;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.KestrelTests
|
||||
{
|
||||
|
|
@ -9,10 +11,13 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
|
|||
{
|
||||
}
|
||||
|
||||
public TestKestrelTrace(ILogger testLogger) : base(testLogger)
|
||||
public TestKestrelTrace(TestApplicationErrorLogger testLogger) : base(testLogger)
|
||||
{
|
||||
Logger = testLogger;
|
||||
}
|
||||
|
||||
public TestApplicationErrorLogger Logger { get; private set; }
|
||||
|
||||
public override void ConnectionRead(string connectionId, int count)
|
||||
{
|
||||
//_logger.LogDebug(1, @"Connection id ""{ConnectionId}"" recv {count} bytes.", connectionId, count);
|
||||
|
|
|
|||
Loading…
Reference in New Issue