Transport Fallback (#1455)

This commit is contained in:
Mikael Mengistu 2018-03-08 06:49:03 +00:00 committed by GitHub
parent 98e270ea6a
commit adbd964efd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 171 additions and 34 deletions

View File

@ -95,11 +95,11 @@ namespace Microsoft.AspNetCore.Sockets.Client
_httpConnectionClosed(logger, null); _httpConnectionClosed(logger, null);
} }
public static void StartingTransport(ILogger logger, ITransport transport, Uri url) public static void StartingTransport(ILogger logger, TransportType transportType, Uri url)
{ {
if (logger.IsEnabled(LogLevel.Debug)) if (logger.IsEnabled(LogLevel.Debug))
{ {
_startingTransport(logger, transport.GetType().Name, url, null); _startingTransport(logger, transportType.ToString(), url, null);
} }
} }

View File

@ -47,6 +47,9 @@ namespace Microsoft.AspNetCore.Sockets.Client
private PipeWriter Output => _transportChannel.Output; private PipeWriter Output => _transportChannel.Output;
private readonly List<ReceiveCallback> _callbacks = new List<ReceiveCallback>(); private readonly List<ReceiveCallback> _callbacks = new List<ReceiveCallback>();
private readonly TransportType _requestedTransportType = TransportType.All; private readonly TransportType _requestedTransportType = TransportType.All;
private TransportType _serverTransports = TransportType.All;
// The order of the transports here is the order determines the fallback order.
private static readonly TransportType[] AllTransports = new[]{ TransportType.WebSockets, TransportType.ServerSentEvents, TransportType.LongPolling };
private readonly ConnectionLogScope _logScope; private readonly ConnectionLogScope _logScope;
private readonly IDisposable _scopeDisposable; private readonly IDisposable _scopeDisposable;
@ -142,6 +145,14 @@ namespace Microsoft.AspNetCore.Sockets.Client
return _startTcs.Task; return _startTcs.Task;
} }
private async Task<NegotiationResponse> GetNegotiationResponse()
{
var negotiationResponse = await Negotiate(Url, _httpClient, _logger);
_connectionId = negotiationResponse.ConnectionId;
_logScope.ConnectionId = _connectionId;
return negotiationResponse;
}
private async Task StartAsyncInternal() private async Task StartAsyncInternal()
{ {
Log.HttpConnectionStarting(_logger); Log.HttpConnectionStarting(_logger);
@ -151,13 +162,12 @@ namespace Microsoft.AspNetCore.Sockets.Client
var connectUrl = Url; var connectUrl = Url;
if (_requestedTransportType == TransportType.WebSockets) if (_requestedTransportType == TransportType.WebSockets)
{ {
_transport = _transportFactory.CreateTransport(TransportType.WebSockets); Log.StartingTransport(_logger, _requestedTransportType, connectUrl);
await StartTransport(connectUrl, _requestedTransportType);
} }
else else
{ {
var negotiationResponse = await Negotiate(Url, _httpClient, _logger); var negotiationResponse = await GetNegotiationResponse();
_connectionId = negotiationResponse.ConnectionId;
_logScope.ConnectionId = _connectionId;
// Connection is being disposed while start was in progress // Connection is being disposed while start was in progress
if (_connectionState == ConnectionState.Disposed) if (_connectionState == ConnectionState.Disposed)
@ -166,13 +176,43 @@ namespace Microsoft.AspNetCore.Sockets.Client
return; return;
} }
_transport = _transportFactory.CreateTransport(GetAvailableServerTransports(negotiationResponse)); // This should only need to happen once
connectUrl = CreateConnectUrl(Url, negotiationResponse); _serverTransports = GetAvailableServerTransports(negotiationResponse);
} connectUrl = CreateConnectUrl(Url, negotiationResponse.ConnectionId);
Log.StartingTransport(_logger, _transport, connectUrl); foreach (var transport in AllTransports)
await StartTransport(connectUrl); {
try
{
if ((transport & _serverTransports & _requestedTransportType) != 0)
{
// The negotiation response gets cleared in the fallback scenario.
if (negotiationResponse == null)
{
negotiationResponse = await GetNegotiationResponse();
connectUrl = CreateConnectUrl(Url, negotiationResponse.ConnectionId);
}
Log.StartingTransport(_logger, transport, connectUrl);
await StartTransport(connectUrl, transport);
break;
}
}
catch (Exception)
{
// Try the next transport
// Clear the negotiation response so we know to re-negotiate.
negotiationResponse = null;
}
}
}
if (_transport == null)
{
throw new InvalidOperationException("Unable to connect to the server with any of the available transports.");
}
} }
catch catch
{ {
// The connection can now be either in the Connecting or Disposed state - only change the state to // The connection can now be either in the Connecting or Disposed state - only change the state to
@ -314,21 +354,23 @@ namespace Microsoft.AspNetCore.Sockets.Client
return availableServerTransports; return availableServerTransports;
} }
private static Uri CreateConnectUrl(Uri url, NegotiationResponse negotiationResponse) private static Uri CreateConnectUrl(Uri url, string connectionId)
{ {
if (string.IsNullOrWhiteSpace(negotiationResponse.ConnectionId)) if (string.IsNullOrWhiteSpace(connectionId))
{ {
throw new FormatException("Invalid connection id returned in negotiation response."); throw new FormatException("Invalid connection id.");
} }
return Utils.AppendQueryString(url, "id=" + negotiationResponse.ConnectionId); return Utils.AppendQueryString(url, "id=" + connectionId);
} }
private async Task StartTransport(Uri connectUrl) private async Task StartTransport(Uri connectUrl, TransportType transportType)
{ {
var options = new PipeOptions(readerScheduler: PipeScheduler.ThreadPool); var options = new PipeOptions(readerScheduler: PipeScheduler.ThreadPool);
var pair = DuplexPipe.CreateConnectionPair(options, options); var pair = DuplexPipe.CreateConnectionPair(options, options);
_transportChannel = pair.Transport; _transportChannel = pair.Transport;
_transport = _transportFactory.CreateTransport(transportType);
// Start the transport, giving it one end of the pipeline // Start the transport, giving it one end of the pipeline
try try
@ -346,6 +388,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
catch (Exception ex) catch (Exception ex)
{ {
Log.ErrorStartingTransport(_logger, _transport, ex); Log.ErrorStartingTransport(_logger, _transport, ex);
_transport = null;
throw; throw;
} }
} }

View File

@ -108,20 +108,22 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
} }
} }
[Fact] [Theory]
public async Task CanStartConnectionThatFailedToStart() [InlineData(2)]
[InlineData(3)]
public async Task TransportThatFailsToStartOnceFallsBack(int passThreshold)
{ {
using (StartLog(out var loggerFactory)) using (StartLog(out var loggerFactory))
{ {
var startCounter = 0;
var expected = new Exception("Transport failed to start"); var expected = new Exception("Transport failed to start");
var shouldFail = true;
Task OnTransportStart() Task OnTransportStart()
{ {
if (shouldFail) startCounter++;
if (startCounter < passThreshold)
{ {
// Succeed next time // Succeed next time
shouldFail = false;
return Task.FromException(expected); return Task.FromException(expected);
} }
else else
@ -136,17 +138,39 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
transport: new TestTransport(onTransportStart: OnTransportStart)), transport: new TestTransport(onTransportStart: OnTransportStart)),
async (connection, closed) => async (connection, closed) =>
{ {
var actual = await Assert.ThrowsAsync<Exception>(() => connection.StartAsync()); Assert.Equal(0, startCounter);
Assert.Same(expected, actual); await connection.StartAsync();
Assert.Equal(passThreshold, startCounter);
// Should succeed this time
shouldFail = false;
await connection.StartAsync().OrTimeout();
}); });
} }
} }
[Fact]
public async Task StartThrowsAfterAllTransportsFail()
{
using (StartLog(out var loggerFactory))
{
var startCounter = 0;
var expected = new Exception("Transport failed to start");
Task OnTransportStart()
{
startCounter++;
return Task.FromException(expected);
}
await WithConnectionAsync(
CreateConnection(
loggerFactory: loggerFactory,
transport: new TestTransport(onTransportStart: OnTransportStart)),
async (connection, closed) =>
{
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => connection.StartAsync());
Assert.Equal("Unable to connect to the server with any of the available transports.", ex.Message);
Assert.Equal(3, startCounter);
});
}
}
[Fact] [Fact]
public async Task CanStartStoppedConnection() public async Task CanStartStoppedConnection()
{ {
@ -214,9 +238,9 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
var httpHandler = new TestHttpMessageHandler(); var httpHandler = new TestHttpMessageHandler();
var longPollResult = new TaskCompletionSource<HttpResponseMessage>(); var longPollResult = new TaskCompletionSource<HttpResponseMessage>();
httpHandler.OnLongPoll(cancellationToken => httpHandler.OnLongPoll(cancellationToken =>
{ {
cancellationToken.Register(() => cancellationToken.Register(() =>
{ {
longPollResult.TrySetResult(ResponseUtils.CreateResponse(HttpStatusCode.NoContent)); longPollResult.TrySetResult(ResponseUtils.CreateResponse(HttpStatusCode.NoContent));
}); });

View File

@ -26,7 +26,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
[Fact] [Fact]
public Task StartThrowsFormatExceptionIfNegotiationResponseHasNoConnectionId() public Task StartThrowsFormatExceptionIfNegotiationResponseHasNoConnectionId()
{ {
return RunInvalidNegotiateResponseTest<FormatException>(ResponseUtils.CreateNegotiationContent(connectionId: null), "Invalid connection id returned in negotiation response."); return RunInvalidNegotiateResponseTest<FormatException>(ResponseUtils.CreateNegotiationContent(connectionId: null), "Invalid connection id.");
} }
[Fact] [Fact]
@ -40,7 +40,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
[InlineData(TransportType.ServerSentEvents)] [InlineData(TransportType.ServerSentEvents)]
public Task ConnectionCannotBeStartedIfNoCommonTransportsBetweenClientAndServer(TransportType serverTransports) public Task ConnectionCannotBeStartedIfNoCommonTransportsBetweenClientAndServer(TransportType serverTransports)
{ {
return RunInvalidNegotiateResponseTest<InvalidOperationException>(ResponseUtils.CreateNegotiationContent(transportTypes: serverTransports), "No requested transports available on the server."); return RunInvalidNegotiateResponseTest<InvalidOperationException>(ResponseUtils.CreateNegotiationContent(transportTypes: serverTransports), "Unable to connect to the server with any of the available transports.");
} }
[Theory] [Theory]

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO.Pipelines;
using System.Net.Http; using System.Net.Http;
using System.Net.WebSockets; using System.Net.WebSockets;
using System.Text; using System.Text;
@ -14,6 +15,7 @@ using Microsoft.AspNetCore.Sockets.Client;
using Microsoft.AspNetCore.Sockets.Client.Http; using Microsoft.AspNetCore.Sockets.Client.Http;
using Microsoft.AspNetCore.Sockets.Features; using Microsoft.AspNetCore.Sockets.Features;
using Microsoft.AspNetCore.Testing.xunit; using Microsoft.AspNetCore.Testing.xunit;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Logging.Testing;
using Moq; using Moq;
@ -55,6 +57,19 @@ namespace Microsoft.AspNetCore.SignalR.Tests
await connection.DisposeAsync().OrTimeout(); await connection.DisposeAsync().OrTimeout();
} }
[Fact]
public async Task TransportThatFallsbackCreatesNewConnection()
{
var url = _serverFixture.Url + "/echo";
// The test should connect to the server using WebSockets transport on Windows 8 and newer.
// On Windows 7/2008R2 it should use ServerSentEvents transport to connect to the server.
// The test logic lives in the TestTransportFactory and FakeTransport.
var connection = new HttpConnection(new Uri(url), new TestTransportFactory(), null, null);
await connection.StartAsync().OrTimeout();
await connection.DisposeAsync().OrTimeout();
}
[Theory] [Theory]
[MemberData(nameof(TransportTypes))] [MemberData(nameof(TransportTypes))]
public async Task CanStartAndStopConnectionUsingGivenTransport(TransportType transportType) public async Task CanStartAndStopConnectionUsingGivenTransport(TransportType transportType)
@ -333,7 +348,6 @@ namespace Microsoft.AspNetCore.SignalR.Tests
var logger = loggerFactory.CreateLogger<EndToEndTests>(); var logger = loggerFactory.CreateLogger<EndToEndTests>();
var url = _serverFixture.Url + "/uncreatable"; var url = _serverFixture.Url + "/uncreatable";
var connection = new HubConnectionBuilder() var connection = new HubConnectionBuilder()
.WithUrl(new Uri(url)) .WithUrl(new Uri(url))
.WithTransport(transportType) .WithTransport(transportType)
@ -384,6 +398,62 @@ namespace Microsoft.AspNetCore.SignalR.Tests
} }
} }
// Serves a fake transport that lets us verify fallback behavior
private class TestTransportFactory : ITransportFactory
{
private ITransport _transport;
public ITransport CreateTransport(TransportType availableServerTransports)
{
if (_transport == null)
{
_transport = new FakeTransport();
}
return _transport;
}
}
private class FakeTransport : ITransport
{
public TransferMode? Mode => TransferMode.Text;
public string prevConnectionId = null;
private int tries = 0;
private IDuplexPipe _application;
public Task StartAsync(Uri url, IDuplexPipe application, TransferMode requestedTransferMode, IConnection connection)
{
_application = application;
tries++;
Assert.True(QueryHelpers.ParseQuery(url.Query.ToString()).TryGetValue("id", out var id));
if (prevConnectionId == null)
{
prevConnectionId = id;
}
else
{
Assert.True(prevConnectionId != id);
prevConnectionId = id;
}
if (tries < 3)
{
throw new Exception();
}
else
{
return Task.CompletedTask;
}
}
public Task StopAsync()
{
_application.Output.Complete();
_application.Input.Complete();
return Task.CompletedTask;
}
}
public static IEnumerable<object[]> TransportTypes public static IEnumerable<object[]> TransportTypes
{ {
get get