Enabling feedback to SendAsync via asymmetric channels
This commit is contained in:
parent
62c3c15a1f
commit
abc9109cf3
|
|
@ -16,14 +16,15 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
||||||
{
|
{
|
||||||
private readonly ILoggerFactory _loggerFactory;
|
private readonly ILoggerFactory _loggerFactory;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
private volatile int _connectionState = ConnectionState.Initial;
|
private volatile int _connectionState = ConnectionState.Initial;
|
||||||
private volatile IChannelConnection<Message> _transportChannel;
|
private volatile IChannelConnection<Message, SendMessage> _transportChannel;
|
||||||
private volatile ITransport _transport;
|
private volatile ITransport _transport;
|
||||||
private volatile Task _receiveLoopTask;
|
private volatile Task _receiveLoopTask;
|
||||||
private volatile Task _startTask = Task.CompletedTask;
|
private volatile Task _startTask = Task.CompletedTask;
|
||||||
|
|
||||||
private ReadableChannel<Message> Input => _transportChannel.Input;
|
private ReadableChannel<Message> Input => _transportChannel.Input;
|
||||||
private WritableChannel<Message> Output => _transportChannel.Output;
|
private WritableChannel<SendMessage> Output => _transportChannel.Output;
|
||||||
|
|
||||||
public Uri Url { get; }
|
public Uri Url { get; }
|
||||||
|
|
||||||
|
|
@ -146,11 +147,11 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
||||||
|
|
||||||
private async Task StartTransport(Uri connectUrl)
|
private async Task StartTransport(Uri connectUrl)
|
||||||
{
|
{
|
||||||
var applicationToTransport = Channel.CreateUnbounded<Message>();
|
var applicationToTransport = Channel.CreateUnbounded<SendMessage>();
|
||||||
var transportToApplication = Channel.CreateUnbounded<Message>();
|
var transportToApplication = Channel.CreateUnbounded<Message>();
|
||||||
var applicationSide = new ChannelConnection<Message>(transportToApplication, applicationToTransport);
|
var applicationSide = new ChannelConnection<SendMessage, Message>(applicationToTransport, transportToApplication);
|
||||||
|
|
||||||
_transportChannel = new ChannelConnection<Message>(applicationToTransport, transportToApplication);
|
_transportChannel = new ChannelConnection<Message, SendMessage>(transportToApplication, applicationToTransport);
|
||||||
|
|
||||||
// Start the transport, giving it one end of the pipeline
|
// Start the transport, giving it one end of the pipeline
|
||||||
try
|
try
|
||||||
|
|
@ -194,12 +195,12 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
||||||
_logger.LogTrace("Ending receive loop");
|
_logger.LogTrace("Ending receive loop");
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<bool> SendAsync(byte[] data, MessageType type)
|
public Task SendAsync(byte[] data, MessageType type)
|
||||||
{
|
{
|
||||||
return SendAsync(data, type, CancellationToken.None);
|
return SendAsync(data, type, CancellationToken.None);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> SendAsync(byte[] data, MessageType type, CancellationToken cancellationToken)
|
public async Task SendAsync(byte[] data, MessageType type, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (data == null)
|
if (data == null)
|
||||||
{
|
{
|
||||||
|
|
@ -208,20 +209,25 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
||||||
|
|
||||||
if (_connectionState != ConnectionState.Connected)
|
if (_connectionState != ConnectionState.Connected)
|
||||||
{
|
{
|
||||||
return false;
|
throw new InvalidOperationException(
|
||||||
|
"Cannot send messages when the connection is not in the Connected state.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var message = new Message(data, type);
|
// TaskCreationOptions.RunContinuationsAsynchronously ensures that continuations awaiting
|
||||||
|
// SendAsync (i.e. user's code) are not running on the same thread as the code that sets
|
||||||
|
// TaskCompletionSource result. This way we prevent from user's code blocking our channel
|
||||||
|
// send loop.
|
||||||
|
var sendTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
var message = new SendMessage(data, type, sendTcs);
|
||||||
|
|
||||||
while (await Output.WaitToWriteAsync(cancellationToken))
|
while (await Output.WaitToWriteAsync(cancellationToken))
|
||||||
{
|
{
|
||||||
if (Output.TryWrite(message))
|
if (Output.TryWrite(message))
|
||||||
{
|
{
|
||||||
return true;
|
await sendTcs.Task;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DisposeAsync()
|
public async Task DisposeAsync()
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
||||||
public interface IConnection
|
public interface IConnection
|
||||||
{
|
{
|
||||||
Task StartAsync(ITransport transport, HttpClient httpClient);
|
Task StartAsync(ITransport transport, HttpClient httpClient);
|
||||||
Task<bool> SendAsync(byte[] data, MessageType type, CancellationToken cancellationToken);
|
Task SendAsync(byte[] data, MessageType type, CancellationToken cancellationToken);
|
||||||
Task DisposeAsync();
|
Task DisposeAsync();
|
||||||
|
|
||||||
event Action Connected;
|
event Action Connected;
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
||||||
{
|
{
|
||||||
public interface ITransport
|
public interface ITransport
|
||||||
{
|
{
|
||||||
Task StartAsync(Uri url, IChannelConnection<Message> application);
|
Task StartAsync(Uri url, IChannelConnection<SendMessage, Message> application);
|
||||||
Task StopAsync();
|
Task StopAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
||||||
|
|
||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private IChannelConnection<Message> _application;
|
private IChannelConnection<SendMessage, Message> _application;
|
||||||
private Task _sender;
|
private Task _sender;
|
||||||
private Task _poller;
|
private Task _poller;
|
||||||
private readonly CancellationTokenSource _transportCts = new CancellationTokenSource();
|
private readonly CancellationTokenSource _transportCts = new CancellationTokenSource();
|
||||||
|
|
@ -40,7 +40,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
||||||
_logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger<LongPollingTransport>();
|
_logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger<LongPollingTransport>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task StartAsync(Uri url, IChannelConnection<Message> application)
|
public Task StartAsync(Uri url, IChannelConnection<SendMessage, Message> application)
|
||||||
{
|
{
|
||||||
_application = application;
|
_application = application;
|
||||||
|
|
||||||
|
|
@ -145,12 +145,14 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
||||||
|
|
||||||
private async Task SendMessages(Uri sendUrl, CancellationToken cancellationToken)
|
private async Task SendMessages(Uri sendUrl, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
TaskCompletionSource<object> sendTcs = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
while (await _application.Input.WaitToReadAsync(cancellationToken))
|
while (await _application.Input.WaitToReadAsync(cancellationToken))
|
||||||
{
|
{
|
||||||
while (!cancellationToken.IsCancellationRequested && _application.Input.TryRead(out Message message))
|
while (!cancellationToken.IsCancellationRequested && _application.Input.TryRead(out SendMessage message))
|
||||||
{
|
{
|
||||||
|
sendTcs = message.SendResult;
|
||||||
var request = new HttpRequestMessage(HttpMethod.Post, sendUrl);
|
var request = new HttpRequestMessage(HttpMethod.Post, sendUrl);
|
||||||
request.Headers.UserAgent.Add(DefaultUserAgentHeader);
|
request.Headers.UserAgent.Add(DefaultUserAgentHeader);
|
||||||
|
|
||||||
|
|
@ -161,16 +163,19 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
||||||
|
|
||||||
var response = await _httpClient.SendAsync(request);
|
var response = await _httpClient.SendAsync(request);
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
|
sendTcs.SetResult(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
// transport is being closed
|
// transport is being closed
|
||||||
|
sendTcs?.TrySetCanceled();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError("Error while sending to '{0}': {1}", sendUrl, ex);
|
_logger.LogError("Error while sending to '{0}': {1}", sendUrl, ex);
|
||||||
|
sendTcs?.TrySetException(ex);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
// 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.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Sockets.Client
|
||||||
|
{
|
||||||
|
public struct SendMessage
|
||||||
|
{
|
||||||
|
public MessageType Type { get; }
|
||||||
|
public byte[] Payload { get; }
|
||||||
|
public TaskCompletionSource<object> SendResult { get; }
|
||||||
|
|
||||||
|
public SendMessage(byte[] payload, MessageType type, TaskCompletionSource<object> result)
|
||||||
|
{
|
||||||
|
Type = type;
|
||||||
|
Payload = payload;
|
||||||
|
SendResult = result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
||||||
public class WebSocketsTransport : ITransport
|
public class WebSocketsTransport : ITransport
|
||||||
{
|
{
|
||||||
private ClientWebSocket _webSocket = new ClientWebSocket();
|
private ClientWebSocket _webSocket = new ClientWebSocket();
|
||||||
private IChannelConnection<Message> _application;
|
private IChannelConnection<SendMessage, Message> _application;
|
||||||
private CancellationToken _cancellationToken = new CancellationToken();
|
private CancellationToken _cancellationToken = new CancellationToken();
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
|
|
@ -26,12 +26,12 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
||||||
|
|
||||||
public WebSocketsTransport(ILoggerFactory loggerFactory)
|
public WebSocketsTransport(ILoggerFactory loggerFactory)
|
||||||
{
|
{
|
||||||
_logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger("WebSocketsTransport");
|
_logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger(nameof(WebSocketsTransport));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task Running { get; private set; } = Task.CompletedTask;
|
public Task Running { get; private set; } = Task.CompletedTask;
|
||||||
|
|
||||||
public async Task StartAsync(Uri url, IChannelConnection<Message> application)
|
public async Task StartAsync(Uri url, IChannelConnection<SendMessage, Message> application)
|
||||||
{
|
{
|
||||||
if (url == null)
|
if (url == null)
|
||||||
{
|
{
|
||||||
|
|
@ -121,21 +121,29 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
||||||
{
|
{
|
||||||
while (await _application.Input.WaitToReadAsync(cancellationToken))
|
while (await _application.Input.WaitToReadAsync(cancellationToken))
|
||||||
{
|
{
|
||||||
Message message;
|
while (_application.Input.TryRead(out SendMessage message))
|
||||||
while (_application.Input.TryRead(out message))
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _webSocket.SendAsync(new ArraySegment<byte>(message.Payload),
|
await _webSocket.SendAsync(new ArraySegment<byte>(message.Payload),
|
||||||
message.Type == MessageType.Text ? WebSocketMessageType.Text : WebSocketMessageType.Binary, true,
|
message.Type == MessageType.Text ? WebSocketMessageType.Text : WebSocketMessageType.Binary, true,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
message.SendResult.SetResult(null);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException ex)
|
catch (OperationCanceledException ex)
|
||||||
{
|
{
|
||||||
_logger?.LogError(ex.Message);
|
_logger?.LogError(ex.Message);
|
||||||
|
message.SendResult.SetCanceled();
|
||||||
await _webSocket.CloseAsync(WebSocketCloseStatus.Empty, null, _cancellationToken);
|
await _webSocket.CloseAsync(WebSocketCloseStatus.Empty, null, _cancellationToken);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger?.LogError(ex.Message);
|
||||||
|
message.SendResult.SetException(ex);
|
||||||
|
await _webSocket.CloseAsync(WebSocketCloseStatus.Empty, null, _cancellationToken);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,13 @@ namespace Microsoft.AspNetCore.Sockets
|
||||||
{
|
{
|
||||||
// REVIEW: These should probably move to Channels. Why not use IChannel? Because I think it's better to be clear that this is providing
|
// REVIEW: These should probably move to Channels. Why not use IChannel? Because I think it's better to be clear that this is providing
|
||||||
// access to two separate channels, the read end for one and the write end for the other.
|
// access to two separate channels, the read end for one and the write end for the other.
|
||||||
public interface IChannelConnection<T> : IDisposable
|
public interface IChannelConnection<T> : IChannelConnection<T, T>
|
||||||
{
|
{
|
||||||
ReadableChannel<T> Input { get; }
|
}
|
||||||
WritableChannel<T> Output { get; }
|
|
||||||
|
public interface IChannelConnection<TIn, TOut> : IDisposable
|
||||||
|
{
|
||||||
|
ReadableChannel<TIn> Input { get; }
|
||||||
|
WritableChannel<TOut> Output { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,21 +7,33 @@ namespace Microsoft.AspNetCore.Sockets.Internal
|
||||||
{
|
{
|
||||||
public static class ChannelConnection
|
public static class ChannelConnection
|
||||||
{
|
{
|
||||||
|
public static ChannelConnection<TIn, TOut> Create<TIn, TOut>(Channel<TIn> input, Channel<TOut> output)
|
||||||
|
{
|
||||||
|
return new ChannelConnection<TIn, TOut>(input, output);
|
||||||
|
}
|
||||||
|
|
||||||
public static ChannelConnection<T> Create<T>(Channel<T> input, Channel<T> output)
|
public static ChannelConnection<T> Create<T>(Channel<T> input, Channel<T> output)
|
||||||
{
|
{
|
||||||
return new ChannelConnection<T>(input, output);
|
return new ChannelConnection<T>(input, output);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ChannelConnection<T> : IChannelConnection<T>
|
public class ChannelConnection<T> : ChannelConnection<T, T>, IChannelConnection<T>
|
||||||
{
|
{
|
||||||
public Channel<T> Input { get; }
|
|
||||||
public Channel<T> Output { get; }
|
|
||||||
|
|
||||||
ReadableChannel<T> IChannelConnection<T>.Input => Input;
|
|
||||||
WritableChannel<T> IChannelConnection<T>.Output => Output;
|
|
||||||
|
|
||||||
public ChannelConnection(Channel<T> input, Channel<T> output)
|
public ChannelConnection(Channel<T> input, Channel<T> output)
|
||||||
|
: base(input, output)
|
||||||
|
{ }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ChannelConnection<TIn, TOut> : IChannelConnection<TIn, TOut>
|
||||||
|
{
|
||||||
|
public Channel<TIn> Input { get; }
|
||||||
|
public Channel<TOut> Output { get; }
|
||||||
|
|
||||||
|
ReadableChannel<TIn> IChannelConnection<TIn, TOut>.Input => Input;
|
||||||
|
WritableChannel<TOut> IChannelConnection<TIn, TOut>.Output => Output;
|
||||||
|
|
||||||
|
public ChannelConnection(Channel<TIn> input, Channel<TOut> output)
|
||||||
{
|
{
|
||||||
Input = input;
|
Input = input;
|
||||||
Output = output;
|
Output = output;
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,8 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
||||||
var transportToApplication = Channel.CreateUnbounded<Message>();
|
var transportToApplication = Channel.CreateUnbounded<Message>();
|
||||||
var applicationToTransport = Channel.CreateUnbounded<Message>();
|
var applicationToTransport = Channel.CreateUnbounded<Message>();
|
||||||
|
|
||||||
Application = ChannelConnection.Create(input: applicationToTransport, output: transportToApplication);
|
Application = ChannelConnection.Create<Message>(input: applicationToTransport, output: transportToApplication);
|
||||||
var transport = ChannelConnection.Create(input: transportToApplication, output: applicationToTransport);
|
var transport = ChannelConnection.Create<Message>(input: transportToApplication, output: applicationToTransport);
|
||||||
|
|
||||||
Connection = new Connection(Guid.NewGuid().ToString(), transport);
|
Connection = new Connection(Guid.NewGuid().ToString(), transport);
|
||||||
Connection.Metadata["formatType"] = format;
|
Connection.Metadata["formatType"] = format;
|
||||||
|
|
|
||||||
|
|
@ -144,19 +144,21 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
||||||
releaseDisposeTcs.SetResult(null);
|
releaseDisposeTcs.SetResult(null);
|
||||||
await disposeTask.OrTimeout();
|
await disposeTask.OrTimeout();
|
||||||
|
|
||||||
transport.Verify(t => t.StartAsync(It.IsAny<Uri>(), It.IsAny<IChannelConnection<Message>>()), Times.Never);
|
transport.Verify(t => t.StartAsync(It.IsAny<Uri>(), It.IsAny<IChannelConnection<SendMessage, Message>>()), Times.Never);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task SendReturnsFalseIfConnectionIsNotStarted()
|
public async Task SendThrowsIfConnectionIsNotStarted()
|
||||||
{
|
{
|
||||||
var connection = new Connection(new Uri("http://fakeuri.org/"));
|
var connection = new Connection(new Uri("http://fakeuri.org/"));
|
||||||
Assert.False(await connection.SendAsync(new byte[0], MessageType.Binary));
|
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||||
|
async () => await connection.SendAsync(new byte[0], MessageType.Binary));
|
||||||
|
Assert.Equal("Cannot send messages when the connection is not in the Connected state.", exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task SendReturnsFalseIfConnectionIsDisposed()
|
public async Task SendThrowsIfConnectionIsDisposed()
|
||||||
{
|
{
|
||||||
var mockHttpHandler = new Mock<HttpMessageHandler>();
|
var mockHttpHandler = new Mock<HttpMessageHandler>();
|
||||||
mockHttpHandler.Protected()
|
mockHttpHandler.Protected()
|
||||||
|
|
@ -175,7 +177,9 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
||||||
await connection.StartAsync(longPollingTransport, httpClient);
|
await connection.StartAsync(longPollingTransport, httpClient);
|
||||||
await connection.DisposeAsync();
|
await connection.DisposeAsync();
|
||||||
|
|
||||||
Assert.False(await connection.SendAsync(new byte[0], MessageType.Binary));
|
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||||
|
async () => await connection.SendAsync(new byte[0], MessageType.Binary));
|
||||||
|
Assert.Equal("Cannot send messages when the connection is not in the Connected state.", exception.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -224,7 +228,7 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
||||||
});
|
});
|
||||||
|
|
||||||
var mockTransport = new Mock<ITransport>();
|
var mockTransport = new Mock<ITransport>();
|
||||||
mockTransport.Setup(t => t.StartAsync(It.IsAny<Uri>(), It.IsAny<IChannelConnection<Message>>()))
|
mockTransport.Setup(t => t.StartAsync(It.IsAny<Uri>(), It.IsAny<IChannelConnection<SendMessage, Message>>()))
|
||||||
.Returns(Task.FromException(new InvalidOperationException("Transport failed to start")));
|
.Returns(Task.FromException(new InvalidOperationException("Transport failed to start")));
|
||||||
|
|
||||||
using (var httpClient = new HttpClient(mockHttpHandler.Object))
|
using (var httpClient = new HttpClient(mockHttpHandler.Object))
|
||||||
|
|
@ -249,7 +253,7 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ClosedEventRaisedWhenTheClientIsStopped()
|
public async Task ClosedEventRaisedWhenTheClientIsBeingStopped()
|
||||||
{
|
{
|
||||||
var mockHttpHandler = new Mock<HttpMessageHandler>();
|
var mockHttpHandler = new Mock<HttpMessageHandler>();
|
||||||
mockHttpHandler.Protected()
|
mockHttpHandler.Protected()
|
||||||
|
|
@ -393,6 +397,79 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SendAsyncThrowsIfConnectionIsNotStarted()
|
||||||
|
{
|
||||||
|
var connection = new Connection(new Uri("http://fakeuri.org/"));
|
||||||
|
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||||
|
async () => await connection.SendAsync(new byte[0], MessageType.Binary));
|
||||||
|
|
||||||
|
Assert.Equal("Cannot send messages when the connection is not in the Connected state.", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SendAsyncThrowsIfConnectionIsDisposed()
|
||||||
|
{
|
||||||
|
var mockHttpHandler = new Mock<HttpMessageHandler>();
|
||||||
|
mockHttpHandler.Protected()
|
||||||
|
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
|
||||||
|
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
|
||||||
|
{
|
||||||
|
await Task.Yield();
|
||||||
|
|
||||||
|
var content = string.Empty;
|
||||||
|
if (request.RequestUri.AbsolutePath.EndsWith("/poll"))
|
||||||
|
{
|
||||||
|
content = "T2:T:42;";
|
||||||
|
}
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(content) };
|
||||||
|
});
|
||||||
|
|
||||||
|
using (var httpClient = new HttpClient(mockHttpHandler.Object))
|
||||||
|
{
|
||||||
|
var longPollingTransport = new LongPollingTransport(httpClient, new LoggerFactory());
|
||||||
|
var connection = new Connection(new Uri("http://fakeuri.org/"));
|
||||||
|
|
||||||
|
await connection.StartAsync(longPollingTransport, httpClient);
|
||||||
|
await connection.DisposeAsync();
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||||
|
async () => await connection.SendAsync(new byte[0], MessageType.Binary));
|
||||||
|
|
||||||
|
Assert.Equal("Cannot send messages when the connection is not in the Connected state.", exception.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CallerReceivesExceptionsFromSendAsync()
|
||||||
|
{
|
||||||
|
var mockHttpHandler = new Mock<HttpMessageHandler>();
|
||||||
|
mockHttpHandler.Protected()
|
||||||
|
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
|
||||||
|
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
|
||||||
|
{
|
||||||
|
await Task.Yield();
|
||||||
|
if (request.RequestUri.AbsolutePath.EndsWith("/send"))
|
||||||
|
{
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.InternalServerError) { Content = new StringContent(string.Empty) };
|
||||||
|
}
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(string.Empty) };
|
||||||
|
});
|
||||||
|
|
||||||
|
using (var httpClient = new HttpClient(mockHttpHandler.Object))
|
||||||
|
{
|
||||||
|
var longPollingTransport = new LongPollingTransport(httpClient, new LoggerFactory());
|
||||||
|
var connection = new Connection(new Uri("http://fakeuri.org/"));
|
||||||
|
|
||||||
|
await connection.StartAsync(longPollingTransport, httpClient);
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<HttpRequestException>(
|
||||||
|
async () => await connection.SendAsync(new byte[0], MessageType.Binary));
|
||||||
|
|
||||||
|
await connection.DisposeAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CanReceiveData()
|
public async Task CanReceiveData()
|
||||||
{
|
{
|
||||||
|
|
@ -443,29 +520,6 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task CannotSendAfterConnectionIsStopped()
|
|
||||||
{
|
|
||||||
var mockHttpHandler = new Mock<HttpMessageHandler>();
|
|
||||||
mockHttpHandler.Protected()
|
|
||||||
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
|
|
||||||
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
|
|
||||||
{
|
|
||||||
await Task.Yield();
|
|
||||||
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(string.Empty) };
|
|
||||||
});
|
|
||||||
|
|
||||||
using (var httpClient = new HttpClient(mockHttpHandler.Object))
|
|
||||||
{
|
|
||||||
var longPollingTransport = new LongPollingTransport(httpClient, new LoggerFactory());
|
|
||||||
var connection = new Connection(new Uri("http://fakeuri.org/"));
|
|
||||||
|
|
||||||
await connection.StartAsync(longPollingTransport, httpClient);
|
|
||||||
await connection.DisposeAsync();
|
|
||||||
Assert.False(await connection.SendAsync(new byte[] { 1, 1, 3, 5, 8 }, MessageType.Binary));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CannotSendAfterReceiveThrewException()
|
public async Task CannotSendAfterReceiveThrewException()
|
||||||
{
|
{
|
||||||
|
|
@ -496,46 +550,10 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
||||||
// Exception in send should shutdown the connection
|
// Exception in send should shutdown the connection
|
||||||
await closeTcs.Task.OrTimeout();
|
await closeTcs.Task.OrTimeout();
|
||||||
|
|
||||||
Assert.False(await connection.SendAsync(new byte[] { 1, 1, 3, 5, 8 }, MessageType.Binary));
|
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||||
}
|
async () => await connection.SendAsync(new byte[0], MessageType.Binary));
|
||||||
finally
|
|
||||||
{
|
|
||||||
await connection.DisposeAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
Assert.Equal("Cannot send messages when the connection is not in the Connected state.", exception.Message);
|
||||||
public async Task CannotReceiveAfterReceiveThrewException()
|
|
||||||
{
|
|
||||||
var mockHttpHandler = new Mock<HttpMessageHandler>();
|
|
||||||
mockHttpHandler.Protected()
|
|
||||||
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
|
|
||||||
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
|
|
||||||
{
|
|
||||||
await Task.Yield();
|
|
||||||
if (request.RequestUri.AbsolutePath.EndsWith("/poll"))
|
|
||||||
{
|
|
||||||
return new HttpResponseMessage(HttpStatusCode.InternalServerError) { Content = new StringContent(string.Empty) };
|
|
||||||
}
|
|
||||||
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(string.Empty) };
|
|
||||||
});
|
|
||||||
|
|
||||||
using (var httpClient = new HttpClient(mockHttpHandler.Object))
|
|
||||||
{
|
|
||||||
var longPollingTransport = new LongPollingTransport(httpClient, new LoggerFactory());
|
|
||||||
var connection = new Connection(new Uri("http://fakeuri.org/"));
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var closeTcs = new TaskCompletionSource<Exception>();
|
|
||||||
connection.Closed += e => closeTcs.TrySetResult(e);
|
|
||||||
|
|
||||||
await connection.StartAsync(longPollingTransport, httpClient);
|
|
||||||
|
|
||||||
// Exception in send should shutdown the connection
|
|
||||||
await closeTcs.Task.OrTimeout();
|
|
||||||
|
|
||||||
Assert.False(await connection.SendAsync(new byte[] { 1, 1, 3, 5, 8 }, MessageType.Binary));
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -38,9 +38,9 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var connectionToTransport = Channel.CreateUnbounded<Message>();
|
var connectionToTransport = Channel.CreateUnbounded<SendMessage>();
|
||||||
var transportToConnection = Channel.CreateUnbounded<Message>();
|
var transportToConnection = Channel.CreateUnbounded<Message>();
|
||||||
var channelConnection = new ChannelConnection<Message>(connectionToTransport, transportToConnection);
|
var channelConnection = new ChannelConnection<SendMessage, Message>(connectionToTransport, transportToConnection);
|
||||||
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), channelConnection);
|
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), channelConnection);
|
||||||
|
|
||||||
transportActiveTask = longPollingTransport.Running;
|
transportActiveTask = longPollingTransport.Running;
|
||||||
|
|
@ -74,9 +74,9 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
||||||
var longPollingTransport = new LongPollingTransport(httpClient, new LoggerFactory());
|
var longPollingTransport = new LongPollingTransport(httpClient, new LoggerFactory());
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var connectionToTransport = Channel.CreateUnbounded<Message>();
|
var connectionToTransport = Channel.CreateUnbounded<SendMessage>();
|
||||||
var transportToConnection = Channel.CreateUnbounded<Message>();
|
var transportToConnection = Channel.CreateUnbounded<Message>();
|
||||||
var channelConnection = new ChannelConnection<Message>(connectionToTransport, transportToConnection);
|
var channelConnection = new ChannelConnection<SendMessage, Message>(connectionToTransport, transportToConnection);
|
||||||
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), channelConnection);
|
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), channelConnection);
|
||||||
|
|
||||||
await longPollingTransport.Running.OrTimeout();
|
await longPollingTransport.Running.OrTimeout();
|
||||||
|
|
@ -106,9 +106,9 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
||||||
var longPollingTransport = new LongPollingTransport(httpClient, new LoggerFactory());
|
var longPollingTransport = new LongPollingTransport(httpClient, new LoggerFactory());
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var connectionToTransport = Channel.CreateUnbounded<Message>();
|
var connectionToTransport = Channel.CreateUnbounded<SendMessage>();
|
||||||
var transportToConnection = Channel.CreateUnbounded<Message>();
|
var transportToConnection = Channel.CreateUnbounded<Message>();
|
||||||
var channelConnection = new ChannelConnection<Message>(connectionToTransport, transportToConnection);
|
var channelConnection = new ChannelConnection<SendMessage, Message>(connectionToTransport, transportToConnection);
|
||||||
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), channelConnection);
|
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), channelConnection);
|
||||||
|
|
||||||
var exception =
|
var exception =
|
||||||
|
|
@ -142,12 +142,12 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
||||||
var longPollingTransport = new LongPollingTransport(httpClient, new LoggerFactory());
|
var longPollingTransport = new LongPollingTransport(httpClient, new LoggerFactory());
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var connectionToTransport = Channel.CreateUnbounded<Message>();
|
var connectionToTransport = Channel.CreateUnbounded<SendMessage>();
|
||||||
var transportToConnection = Channel.CreateUnbounded<Message>();
|
var transportToConnection = Channel.CreateUnbounded<Message>();
|
||||||
var channelConnection = new ChannelConnection<Message>(connectionToTransport, transportToConnection);
|
var channelConnection = new ChannelConnection<SendMessage, Message>(connectionToTransport, transportToConnection);
|
||||||
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), channelConnection);
|
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), channelConnection);
|
||||||
|
|
||||||
await connectionToTransport.Out.WriteAsync(new Message());
|
await connectionToTransport.Out.WriteAsync(new SendMessage());
|
||||||
|
|
||||||
await Assert.ThrowsAsync<HttpRequestException>(async () => await longPollingTransport.Running.OrTimeout());
|
await Assert.ThrowsAsync<HttpRequestException>(async () => await longPollingTransport.Running.OrTimeout());
|
||||||
|
|
||||||
|
|
@ -183,9 +183,9 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
||||||
var longPollingTransport = new LongPollingTransport(httpClient, new LoggerFactory());
|
var longPollingTransport = new LongPollingTransport(httpClient, new LoggerFactory());
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var connectionToTransport = Channel.CreateUnbounded<Message>();
|
var connectionToTransport = Channel.CreateUnbounded<SendMessage>();
|
||||||
var transportToConnection = Channel.CreateUnbounded<Message>();
|
var transportToConnection = Channel.CreateUnbounded<Message>();
|
||||||
var channelConnection = new ChannelConnection<Message>(connectionToTransport, transportToConnection);
|
var channelConnection = new ChannelConnection<SendMessage, Message>(connectionToTransport, transportToConnection);
|
||||||
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), channelConnection);
|
await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), channelConnection);
|
||||||
|
|
||||||
connectionToTransport.Out.Complete();
|
connectionToTransport.Out.Complete();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue