The Wayback Machine - https://web.archive.org/web/20220325124054/https://github.com/dotnet/aspnetcore/pull/40811
Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SignalR] Add client return results #40811

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft

Conversation

BrennanConroy
Copy link
Member

@BrennanConroy BrennanConroy commented Mar 21, 2022

This is the majority of the #5280 work.

There is still some cleanup needed like adding logging and better exceptions/exception messages.
And some additional work needed after this change goes in that I'll list in the issue later.

@BrennanConroy BrennanConroy added the area-signalr label Mar 21, 2022
/// <param name="methodName">The name of the hub method to define.</param>
/// <param name="handler">The handler that will be raised when the hub method is invoked.</param>
/// <returns>A subscription that can be disposed to unsubscribe from the hub method.</returns>
public static IDisposable On<T1, T2, T3, T4, T5, T6, T7, T8, TResult>(this HubConnection hubConnection, string methodName, Func<T1, T2, T3, T4, T5, T6, T7, T8, Task<TResult>> handler)
Copy link
Member

@davidfowl davidfowl Mar 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

8? Not 10?

Copy link
Member Author

@BrennanConroy BrennanConroy Mar 24, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just copied what we had for the non-result On methods.

throw new ArgumentNullException(nameof(hubConnection));
}

return hubConnection.On(methodName,
Copy link
Member

@davidfowl davidfowl Mar 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Man this is hideous 😄

if (resultType == typeof(RawResult))
{
Debug.Assert(((RawResult)message.Result).RawSerializedData.IsSingleSegment);
writer.WriteRawValue(((RawResult)message.Result).RawSerializedData.First.Span, skipInputValidation: true);
Copy link
Member

@davidfowl davidfowl Mar 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.First.Span? Shouldn't this write the whole thing?

Copy link
Member Author

@BrennanConroy BrennanConroy Mar 24, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's what the assert is for, currently we're always allocating a byte[] to store the data.

reader.Skip();
var end = reader.BytesConsumed;
var sequence = input.Slice(start, end - start);
// Review: Technically we could pass the sequence without copying into a new array
Copy link
Member

@davidfowl davidfowl Mar 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

super nit

Suggested change
// Review: Technically we could pass the sequence without copying into a new array
// REVIEW: Technically we could pass the sequence without copying into a new array

var token = JToken.Load(reader);
var str = token.ToString(Formatting.None);
result = new RawResult(new ReadOnlySequence<byte>(Encoding.UTF8.GetBytes(str)));
Copy link
Member

@davidfowl davidfowl Mar 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @JamesNK (JRaw?)

Copy link
Member

@JamesNK JamesNK Mar 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think JRaw is useful here.

However, this code could be optimized. Rather than ToString + GetBytes, you could serialize the JToken directly to a MemoryStream then ToArray. Or even get the underlying array from the memory stream, trim it as Memory<bytes> and create a sequence from that.

Copy link
Member Author

@BrennanConroy BrennanConroy Mar 24, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you suggesting:

var token = JToken.Load(reader);
using var strm = new MemoryStream();
using var writer = new StreamWriter(strm);
using var jsonTextWriter = new JsonTextWriter(writer);
token.WriteTo(jsonTextWriter);
jsonTextWriter.Flush();
writer.Flush();
Memory<byte> buf;
if (strm.TryGetBuffer(out var segment))
{
    buf = segment.Array.AsMemory(segment.Offset, segment.Count);
}
else
{
    buf = strm.ToArray();
}


public (Type Type, string ConnectionId, Func<CompletionMessage, Task> Completion) RemoveInvocation(string invocationId)
{
_pendingInvocations.Remove(invocationId, out var item);
Copy link
Member

@davidfowl davidfowl Mar 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TryRemove?

Suggested change
_pendingInvocations.Remove(invocationId, out var item);
_pendingInvocations.TryRemove(invocationId, out var item);

Copy link
Member Author

@BrennanConroy BrennanConroy Mar 24, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove and TryRemove are the same thing in ConcurrentDictionary, do you prefer TryRemove?

{
if (item.ConnectionId != connectionId)
{
throw new Exception("wrong ID");
Copy link
Member

@davidfowl davidfowl Mar 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: Fix the exception and message?

public RawResult(ReadOnlySequence<byte> rawBytes)
{
// Review: If we want to use an ArrayPool we would need some sort of release mechanism
RawSerializedData = new ReadOnlySequence<byte>(rawBytes.ToArray());
Copy link
Member

@davidfowl davidfowl Mar 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we copying? We should make the caller copy.

Copy link
Member Author

@BrennanConroy BrennanConroy Mar 24, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we can find a clean way to use a pool in this type then we can state in the doc comments that we'll do the copying so they don't need to allocate. But if we decide not to use a pool then we can say callers should copy.

/// <param name="args">The invocation arguments.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is <see cref="CancellationToken.None" />.</param>
/// <returns>The response from the connection.</returns>
public virtual Task<T> InvokeConnectionAsync<T>(string connectionId, string methodName, object?[] args, CancellationToken cancellationToken = default)
Copy link
Member

@davidfowl davidfowl Mar 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we need a SupportsClientResults bool so we can throw a nicer error when implementations don't support it. We can check and throw in the ISingleClientProxy

// TODO: this relies on the lifetime manager keeping state for the return type after deserializing the message, is that ok?
// InvocationId is always required on CompletionMessage, it's nullable because of the base type
else if (_hubLifetimeManager.TryGetReturnType(completionMessage.InvocationId!, out _))
Copy link
Member

@davidfowl davidfowl Mar 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need another method on the HubLifetimeManager here?

_parallelEnabled = parallelEnabled;
}

private class NotParallelSingleClientProxy : ISingleClientProxy
Copy link
Member

@davidfowl davidfowl Mar 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clever!

{
// Written as a MessagePack 'arr' containing at least these items:
// * A MessagePack 'arr' of 'str's representing the excluded ids
// * [The output of WriteSerializedHubMessage, which is an 'arr']
// For invocations expecting a result
Copy link
Member

@davidfowl davidfowl Mar 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is non-breaking right?

Copy link
Member Author

@BrennanConroy BrennanConroy Mar 24, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-signalr
Projects
None yet
Linked issues

Successfully merging this pull request may close these issues.

None yet

3 participants