-
Notifications
You must be signed in to change notification settings - Fork 448
Change the IConnection contract to be an IDuplexPipe #1446
Changes from all commits
30dcee9
8c8901e
26414bb
1809741
d8c7397
9a2140b
362d345
538daec
841c39b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,7 @@ | |
using System.Collections.Generic; | ||
using System.Diagnostics; | ||
using System.IO; | ||
using System.IO.Pipelines; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using System.Threading.Channels; | ||
|
@@ -38,6 +39,8 @@ public class HubConnection | |
private readonly ConcurrentDictionary<string, List<InvocationHandler>> _handlers = new ConcurrentDictionary<string, List<InvocationHandler>>(); | ||
private CancellationTokenSource _connectionActive; | ||
|
||
private Task _readingTask; | ||
|
||
private int _nextId = 0; | ||
private volatile bool _startCalled; | ||
private Timer _timeoutTimer; | ||
|
@@ -68,9 +71,7 @@ public HubConnection(IConnection connection, IHubProtocol protocol, ILoggerFacto | |
_protocol = protocol; | ||
_loggerFactory = loggerFactory ?? NullLoggerFactory.Instance; | ||
_logger = _loggerFactory.CreateLogger<HubConnection>(); | ||
_connection.OnReceived((data, state) => ((HubConnection)state).OnDataReceivedAsync(data), this); | ||
_connection.Closed += e => Shutdown(e); | ||
|
||
// Create the timer for timeout, but disabled by default (we enable it when started). | ||
_timeoutTimer = new Timer(state => ((HubConnection)state).TimeoutElapsed(), this, Timeout.Infinite, Timeout.Infinite); | ||
} | ||
|
@@ -103,7 +104,7 @@ private void ResetTimeoutTimer() | |
// we don't need the timer anyway. | ||
try | ||
{ | ||
_timeoutTimer.Change(ServerTimeout, Timeout.InfiniteTimeSpan); | ||
_timeoutTimer.Change(Debugger.IsAttached ? Timeout.InfiniteTimeSpan : ServerTimeout, Timeout.InfiniteTimeSpan); | ||
} | ||
catch (ObjectDisposedException) | ||
{ | ||
|
@@ -140,9 +141,13 @@ private async Task StartAsyncCore() | |
using (var memoryStream = new MemoryStream()) | ||
{ | ||
NegotiationProtocol.WriteMessage(new NegotiationMessage(_protocol.Name), memoryStream); | ||
await _connection.SendAsync(memoryStream.ToArray(), _connectionActive.Token); | ||
|
||
// TODO: Pass the token when that's available | ||
await _connection.Output.WriteAsync(memoryStream.ToArray()); | ||
} | ||
|
||
_readingTask = StartReading(); | ||
|
||
ResetTimeoutTimer(); | ||
} | ||
|
||
|
@@ -162,14 +167,24 @@ private IDataEncoder GetDataEncoder(TransferMode requestedTransferMode, Transfer | |
|
||
public async Task StopAsync() => await StopAsyncCore().ForceAsync(); | ||
|
||
private Task StopAsyncCore() => _connection.StopAsync(); | ||
private async Task StopAsyncCore() | ||
{ | ||
await _connection.StopAsync(); | ||
|
||
if (_readingTask != null) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you have to capture There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yea, this part of the code is racy. I took a look last night and since the receive loop moved out of the place that had all of the locks and synchronization it's a bit messy. |
||
{ | ||
await _readingTask; | ||
} | ||
} | ||
|
||
public async Task DisposeAsync() => await DisposeAsyncCore().ForceAsync(); | ||
|
||
private async Task DisposeAsyncCore() | ||
{ | ||
await _connection.DisposeAsync(); | ||
|
||
await StopAsync(); | ||
|
||
// Dispose the timer AFTER shutting down the connection. | ||
_timeoutTimer.Dispose(); | ||
} | ||
|
@@ -298,7 +313,11 @@ private async Task SendHubMessage(HubInvocationMessage hubMessage, InvocationReq | |
var payload = _protocolReaderWriter.WriteMessage(hubMessage); | ||
_logger.SendInvocation(hubMessage.InvocationId); | ||
|
||
await _connection.SendAsync(payload, irq.CancellationToken); | ||
// TODO: Pass irq.CancellationToken when that's available | ||
irq.CancellationToken.ThrowIfCancellationRequested(); | ||
|
||
await _connection.Output.WriteAsync(payload); | ||
|
||
_logger.SendInvocationCompleted(hubMessage.InvocationId); | ||
} | ||
catch (Exception ex) | ||
|
@@ -331,7 +350,10 @@ private async Task SendAsyncCore(string methodName, object[] args, CancellationT | |
var payload = _protocolReaderWriter.WriteMessage(invocationMessage); | ||
_logger.SendInvocation(invocationMessage.InvocationId); | ||
|
||
await _connection.SendAsync(payload, cancellationToken); | ||
// TODO: Pass the cancellationToken when that's available | ||
cancellationToken.ThrowIfCancellationRequested(); | ||
|
||
await _connection.Output.WriteAsync(payload); | ||
_logger.SendInvocationCompleted(invocationMessage.InvocationId); | ||
} | ||
catch (Exception ex) | ||
|
@@ -341,47 +363,82 @@ private async Task SendAsyncCore(string methodName, object[] args, CancellationT | |
} | ||
} | ||
|
||
private async Task OnDataReceivedAsync(byte[] data) | ||
private async Task StartReading() | ||
{ | ||
ResetTimeoutTimer(); | ||
if (_protocolReaderWriter.ReadMessages(data, _binder, out var messages)) | ||
try | ||
{ | ||
foreach (var message in messages) | ||
while (true) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you loop on There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should add a couple cancellation checks in the below code as well, maybe after every message There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nah. I’m going to do a pass removing all of the tokens. I hate the fact that it throws an exception for expected behavior. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Most of the pipelines loops look like this FWIW There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Only if you use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That is the contract for cancellation tokens. The operation is cancelled via an exception. Pipelines has an alternative model that I plan to change to once all the code is moved. |
||
{ | ||
InvocationRequest irq; | ||
switch (message) | ||
var result = await _connection.Input.ReadAsync(); | ||
var buffer = result.Buffer; | ||
var consumed = buffer.End; | ||
var examined = buffer.End; | ||
|
||
try | ||
{ | ||
case InvocationMessage invocation: | ||
_logger.ReceivedInvocation(invocation.InvocationId, invocation.Target, | ||
invocation.ArgumentBindingException != null ? null : invocation.Arguments); | ||
await DispatchInvocationAsync(invocation, _connectionActive.Token); | ||
break; | ||
case CompletionMessage completion: | ||
if (!TryRemoveInvocation(completion.InvocationId, out irq)) | ||
{ | ||
_logger.DropCompletionMessage(completion.InvocationId); | ||
return; | ||
} | ||
DispatchInvocationCompletion(completion, irq); | ||
irq.Dispose(); | ||
break; | ||
case StreamItemMessage streamItem: | ||
// Complete the invocation with an error, we don't support streaming (yet) | ||
if (!TryGetInvocation(streamItem.InvocationId, out irq)) | ||
if (!buffer.IsEmpty) | ||
{ | ||
ResetTimeoutTimer(); | ||
|
||
if (_protocolReaderWriter.ReadMessages(buffer, _binder, out var messages, out consumed, out examined)) | ||
{ | ||
_logger.DropStreamMessage(streamItem.InvocationId); | ||
return; | ||
foreach (var message in messages) | ||
{ | ||
InvocationRequest irq; | ||
switch (message) | ||
{ | ||
case InvocationMessage invocation: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Can we get some new lines in here There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is literally copied from what was here before. Where do you want the newlines and does our other code look like that? |
||
_logger.ReceivedInvocation(invocation.InvocationId, invocation.Target, | ||
invocation.ArgumentBindingException != null ? null : invocation.Arguments); | ||
await DispatchInvocationAsync(invocation, _connectionActive.Token); | ||
break; | ||
case CompletionMessage completion: | ||
if (!TryRemoveInvocation(completion.InvocationId, out irq)) | ||
{ | ||
_logger.DropCompletionMessage(completion.InvocationId); | ||
return; | ||
} | ||
DispatchInvocationCompletion(completion, irq); | ||
irq.Dispose(); | ||
break; | ||
case StreamItemMessage streamItem: | ||
// Complete the invocation with an error, we don't support streaming (yet) | ||
if (!TryGetInvocation(streamItem.InvocationId, out irq)) | ||
{ | ||
_logger.DropStreamMessage(streamItem.InvocationId); | ||
return; | ||
} | ||
DispatchInvocationStreamItemAsync(streamItem, irq); | ||
break; | ||
case PingMessage _: | ||
// Nothing to do on receipt of a ping. | ||
break; | ||
default: | ||
throw new InvalidOperationException($"Unexpected message type: {message.GetType().FullName}"); | ||
} | ||
} | ||
} | ||
DispatchInvocationStreamItemAsync(streamItem, irq); | ||
break; | ||
case PingMessage _: | ||
// Nothing to do on receipt of a ping. | ||
|
||
} | ||
else if (result.IsCompleted) | ||
{ | ||
break; | ||
default: | ||
throw new InvalidOperationException($"Unexpected message type: {message.GetType().FullName}"); | ||
} | ||
} | ||
finally | ||
{ | ||
_connection.Input.AdvanceTo(consumed, examined); | ||
} | ||
} | ||
} | ||
catch (Exception ex) | ||
{ | ||
_connection.Input.Complete(ex); | ||
} | ||
finally | ||
{ | ||
_connection.Input.Complete(); | ||
} | ||
} | ||
|
||
private void Shutdown(Exception exception = null) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
// 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; | ||
using System.IO.Pipelines; | ||
using System.Threading; | ||
|
||
namespace Microsoft.AspNetCore.Sockets.Client | ||
{ | ||
public partial class HttpConnection | ||
{ | ||
private class HttpConnectionPipeReader : PipeReader | ||
{ | ||
private readonly HttpConnection _connection; | ||
|
||
public HttpConnectionPipeReader(HttpConnection connection) | ||
{ | ||
_connection = connection; | ||
} | ||
|
||
public override void AdvanceTo(SequencePosition consumed) | ||
{ | ||
_connection._transportChannel.Input.AdvanceTo(consumed); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will NRE if the connection hasn't been started. We should put in a check that provides a better error message. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don’t want to put checks in all of the methods here since some of performance critical. I’ll see what makes sense and update. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll just add the check for now and we can fix it when we do moar performance. |
||
} | ||
|
||
public override void AdvanceTo(SequencePosition consumed, SequencePosition examined) | ||
{ | ||
_connection._transportChannel.Input.AdvanceTo(consumed, examined); | ||
} | ||
|
||
public override void CancelPendingRead() | ||
{ | ||
_connection._transportChannel.Input.CancelPendingRead(); | ||
} | ||
|
||
public override void Complete(Exception exception = null) | ||
{ | ||
_connection._transportChannel.Input.Complete(exception); | ||
} | ||
|
||
public override void OnWriterCompleted(Action<Exception, object> callback, object state) | ||
{ | ||
_connection._transportChannel.Input.OnWriterCompleted(callback, state); | ||
} | ||
|
||
public override ValueAwaiter<ReadResult> ReadAsync(CancellationToken cancellationToken = default) | ||
{ | ||
return _connection._transportChannel.Input.ReadAsync(cancellationToken); | ||
} | ||
|
||
public override bool TryRead(out ReadResult result) | ||
{ | ||
return _connection._transportChannel.Input.TryRead(out result); | ||
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
File a bug
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#1451