Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
a3451e6
add ClientResultsManager
xingsy97 Sep 20, 2022
a33c904
add ClientResultsManager
xingsy97 Sep 20, 2022
b08557f
Merge branch 'dev' into ci-ClientResultsManager
xingsy97 Sep 20, 2022
96058f5
re-design ClientResultsManager
xingsy97 Sep 23, 2022
00348d6
Merge branch 'ci-ClientResultsManager' of https://github.com/xingsy97…
xingsy97 Sep 23, 2022
a03f434
Merge branch 'dev' into ci-ClientResultsManager
xingsy97 Sep 23, 2022
79a0567
main logic for client invocation
xingsy97 Sep 23, 2022
28dc3c6
typo fix
xingsy97 Sep 23, 2022
6b504cf
Merge branch 'ci-ClientResultsManager' of https://github.com/xingsy97…
xingsy97 Sep 23, 2022
f3281af
add ClientInvocation main logic
xingsy97 Sep 23, 2022
790b6c0
bug fix
xingsy97 Sep 23, 2022
f594136
bug fix
xingsy97 Sep 23, 2022
9dccae7
bug fix
xingsy97 Sep 23, 2022
a7c8c8a
update
xingsy97 Sep 23, 2022
517dfaa
update
xingsy97 Sep 23, 2022
cfd493b
update interface, add simple unit tests
xingsy97 Sep 23, 2022
f1b96c1
update
xingsy97 Sep 23, 2022
7a13169
update
xingsy97 Sep 25, 2022
b129204
update
xingsy97 Sep 26, 2022
67c9783
sync interface
xingsy97 Sep 26, 2022
acc313a
update
xingsy97 Sep 26, 2022
6cad3ee
update
xingsy97 Sep 26, 2022
c73d59c
rename, relocation and other updates
xingsy97 Sep 26, 2022
46f1b4c
update RoutedClientResultsManager
xingsy97 Sep 26, 2022
ea7b1bb
update
xingsy97 Sep 26, 2022
f4e143c
sync with PR 1684 and some updates
xingsy97 Sep 27, 2022
ee60530
minor fix
xingsy97 Sep 27, 2022
d2a3178
update about nameProvider
xingsy97 Sep 27, 2022
5cd56c4
update
xingsy97 Sep 27, 2022
0e71e48
Merge branch 'dev' into ci-ClientResultsManager
xingsy97 Sep 27, 2022
2137268
Merge branch 'dev' into ci-ClientResultsManager
xingsy97 Sep 28, 2022
8299c2d
Merge branch 'dev' into ci-main
xingsy97 Sep 28, 2022
d18743a
tmp
xingsy97 Sep 28, 2022
1eec339
add `type` in `AddInvocation` and other updates
xingsy97 Sep 28, 2022
d522182
use RawResult for routed client invocation return type
xingsy97 Sep 29, 2022
557de85
Merge branch 'dev' into ci-ClientResultsManager
xingsy97 Sep 29, 2022
ecf28b4
update
xingsy97 Oct 10, 2022
789427d
Merge branch 'dev' into ci-main
xingsy97 Oct 10, 2022
b7d027e
update
xingsy97 Oct 10, 2022
d48ad23
Merge branch 'ci-ClientResultsManager' of https://github.com/xingsy97…
xingsy97 Oct 10, 2022
55b5fb4
add `BaseClientResultsManager` and `RemoveServiceMappingMessage`
xingsy97 Oct 11, 2022
8811ca0
remove independent mapping information dictionary
xingsy97 Oct 12, 2022
7867dd8
undo comment of ClientCompletionMessage
xingsy97 Oct 12, 2022
c4f7ad7
update
xingsy97 Oct 12, 2022
46eaf5a
update exception
xingsy97 Oct 13, 2022
c5aca5a
update
xingsy97 Oct 13, 2022
80031be
update
xingsy97 Oct 13, 2022
4d196f0
update
xingsy97 Oct 13, 2022
7f5d42e
update
xingsy97 Oct 13, 2022
e296cb8
hubProtocolResolver null check
xingsy97 Oct 13, 2022
15876d6
format fix
xingsy97 Oct 13, 2022
4fc63e5
Merge branch 'ci-main' into ci-ClientResultsManager
xingsy97 Oct 13, 2022
f4e9f8f
Revert bad merge "Merge branch 'ci-main' into ci-ClientResultsManager"
xingsy97 Oct 13, 2022
b97589f
route server won't record RouterInstanceId
xingsy97 Oct 14, 2022
8ec3042
fix bug for `TrySetCanceled`
xingsy97 Oct 14, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using Microsoft.AspNetCore.SignalR;

namespace Microsoft.Azure.SignalR
{
internal sealed class DummyClientInvocationManager : IClientInvocationManager
{
public ICallerClientResultsManager Caller => throw new NotSupportedException();
public IRoutedClientResultsManager Router => throw new NotSupportedException();

public DummyClientInvocationManager()
{
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.SignalR.Protocol;

namespace Microsoft.Azure.SignalR
{
internal interface ICallerClientResultsManager : IClientResultsManager
{
string GenerateInvocationId(string connectionId);

/// <summary>
/// Add a invocation which is directly called by current server
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="connectionId"></param>
/// <param name="invocationId"></param>
/// <param name="instanceId"> The InstanceId of target client the caller server knows when this method is called. If the target client is managed by the caller server, the caller server knows the InstanceId of target client and this parameter is not null. Otherwise, this parameter is null. </param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<T> AddInvocation<T>(string connectionId, string invocationId, string instanceId, CancellationToken cancellationToken);

void AddServiceMapping(ServiceMappingMessage serviceMappingMessage);

void CleanupInvocationsByInstance(string instanceId);

bool TryCompleteResult(string connectionId, ClientCompletionMessage message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Microsoft.Azure.SignalR
{
internal interface IClientInvocationManager
{
ICallerClientResultsManager Caller { get; }
IRoutedClientResultsManager Router { get; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using Microsoft.AspNetCore.SignalR.Protocol;
using Microsoft.Azure.SignalR.Protocol;

namespace Microsoft.Azure.SignalR
{
internal interface IClientResultsManager
{
bool TryCompleteResult(string connectionId, CompletionMessage message);

bool TryGetInvocationReturnType(string invocationId, out Type type);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Threading;

namespace Microsoft.Azure.SignalR
{
internal interface IRoutedClientResultsManager : IClientResultsManager
{
void AddInvocation(string connectionId, string invocationId, string callerServerId, CancellationToken cancellationToken);

bool ContainsInvocation(string invocationId);
Copy link
Member

Choose a reason for hiding this comment

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

This method is no need? you can directly use TryGetInvocationReturnType to check if it contains Invocation?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes.
But considering when we just want to know whether a invocationId exists, using TryGetInvocationReturnType is some kind of confusing.
Because ...ReturnType has nothing to do with our logic.
This method is indeed unnecessary but I'm not sure if it should be removed.


void CleanupInvocationsByConnection(string connectionId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,21 @@
</PropertyGroup>

<PropertyGroup>
<TargetFrameworks>netstandard2.0;netcoreapp3.0;net5.0</TargetFrameworks>
<TargetFrameworks>netstandard2.0;netcoreapp3.0;net5.0;net7.0</TargetFrameworks>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Microsoft.Azure.SignalR.Protocols\Microsoft.Azure.SignalR.Protocols.csproj" />
</ItemGroup>

<ItemGroup Condition=" '$(TargetFrameworkIdentifier)' != '.NETStandard' ">
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>

<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="$(MicrosoftAspNetCoreSignalRPackageVersion)" />
</ItemGroup>

<!-- New packages should be added to Directory.Build.props! -->
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
#if NET7_0_OR_GREATER
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Collections.Concurrent;
using Microsoft.Azure.SignalR.Protocol;
using Microsoft.AspNetCore.SignalR.Protocol;
using Microsoft.AspNetCore.SignalR;

namespace Microsoft.Azure.SignalR
{
internal sealed class CallerClientResultsManager : ICallerClientResultsManager, IInvocationBinder
{
private readonly ConcurrentDictionary<string, PendingInvocation> _pendingInvocations = new();
private readonly string _clientResultManagerId = Guid.NewGuid().ToString("N");
private long _lastInvocationId = 0;

private readonly IHubProtocolResolver _hubProtocolResolver;

public CallerClientResultsManager(IHubProtocolResolver hubProtocolResolver)
{
_hubProtocolResolver = hubProtocolResolver ?? throw new ArgumentNullException(nameof(hubProtocolResolver));
}

public string GenerateInvocationId(string connectionId)
{
return $"{connectionId}-{_clientResultManagerId}-{Interlocked.Increment(ref _lastInvocationId)}";
}

public Task<T> AddInvocation<T>(string connectionId, string invocationId, string instanceId, CancellationToken cancellationToken)
{
var tcs = new TaskCompletionSourceWithCancellation<T>(
cancellationToken,
() => TryCompleteResult(connectionId, CompletionMessage.WithError(invocationId, "Canceled")));

// When the caller server is also the client router, Azure SignalR service won't send a ServiceMappingMessage to server.
// To handle this condition, CallerClientResultsManager itself should record this mapping information rather than waiting for a ServiceMappingMessage sent by service. Only in this condition, this method is called with instanceId != null.
var result = _pendingInvocations.TryAdd(invocationId,
new PendingInvocation(
typeof(T), connectionId, tcs,
static (state, completionMessage) =>
{
var tcs = (TaskCompletionSourceWithCancellation<T>)state;
if (completionMessage.HasResult)
{
tcs.TrySetResult((T)completionMessage.Result);
}
else
{
tcs.TrySetException(new Exception(completionMessage.Error));
}
}) { RouterInstanceId = instanceId }
);
Debug.Assert(result);

tcs.RegisterCancellation();

return tcs.Task;
}

public void AddServiceMapping(ServiceMappingMessage serviceMappingMessage)
{
if (_pendingInvocations.TryGetValue(serviceMappingMessage.InvocationId, out var invocation))
{
if (invocation.RouterInstanceId == null)
{
invocation.RouterInstanceId = serviceMappingMessage.InstanceId;
}
else
{
// do nothing
}
}
else
{
// do nothing
}
}

public void CleanupInvocationsByInstance(string instanceId)
{
foreach (var (invocationId, invocation) in _pendingInvocations)
{
if (invocation.RouterInstanceId == instanceId)
{
var message = new CompletionMessage(invocationId, $"Connection '{invocation.ConnectionId}' is disconnected.", null, false);

invocation.Complete(invocation.Tcs, message);
_pendingInvocations.TryRemove(invocationId, out _);
}
}
}

public bool TryCompleteResult(string connectionId, CompletionMessage message)
{
if (_pendingInvocations.TryGetValue(message.InvocationId, out var item))
{
if (item.ConnectionId != connectionId)
{
// Follow https://github.com/dotnet/aspnetcore/blob/main/src/SignalR/common/Shared/ClientResultsManager.cs#L58
throw new InvalidOperationException($"Connection ID '{connectionId}' is not valid for invocation ID '{message.InvocationId}'.");
}

// if false the connection disconnected right after the above TryGetValue
// or someone else completed the invocation (likely a bad client)
// we'll ignore both cases
if (_pendingInvocations.TryRemove(message.InvocationId, out _))
{
item.Complete(item.Tcs, message);
return true;
}
return false;
}
else
{
// connection was disconnected or someone else completed the invocation
return false;
}
}

public bool TryCompleteResult(string connectionId, ClientCompletionMessage message)
{
var protocol = _hubProtocolResolver.GetProtocol(message.Protocol, null);
if (protocol == null)
{
var errorMessage = $"Not supported protocol {message.Protocol} by server.";
return TryCompleteResult(connectionId, new CompletionMessage(message.InvocationId, errorMessage, null, false));
}

var payload = message.Payload;
if (protocol.TryParseMessage(ref payload, this, out var hubMessage))
{
if (hubMessage is CompletionMessage completionMessage)
{
return TryCompleteResult(connectionId, completionMessage);
}
else
{
throw new InvalidOperationException($"The payload of ClientCompletionMessage whose type is {hubMessage.GetType().Name} cannot be parsed into CompletionMessage correctly.");
}
}
return false;
}

// Implemented for interface IInvocationBinder
public Type GetReturnType(string invocationId)
{
if (TryGetInvocationReturnType(invocationId, out var type))
{
return type;
}
// This exception will be handled by https://github.com/dotnet/aspnetcore/blob/f96dce6889fe67aaed33f0c2b147b8b537358f1e/src/SignalR/common/Shared/TryGetReturnType.cs#L14 with a silent failure. The user won't be interrupted.
throw new InvalidOperationException($"Invocation ID '{invocationId}' is not associated with a pending client result.");
Copy link
Member

Choose a reason for hiding this comment

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

Is it possible that invocation is already removed? What happens to the customer when it throws?

}

public bool TryGetInvocationReturnType(string invocationId, out Type type)
{
if (_pendingInvocations.TryGetValue(invocationId, out var item))
{
type = item.Type;
return true;
}
type = null;
return false;
}

// Unused, here to honor the IInvocationBinder interface but should never be called
public IReadOnlyList<Type> GetParameterTypes(string methodName) => throw new NotImplementedException();

// Unused, here to honor the IInvocationBinder interface but should never be called
public Type GetStreamItemType(string streamId) => throw new NotImplementedException();

private record PendingInvocation(Type Type, string ConnectionId, object Tcs, Action<object, CompletionMessage> Complete)
{
public string RouterInstanceId { get; set; }
}
}
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
#if NET7_0_OR_GREATER
using System;
using Microsoft.AspNetCore.SignalR;

namespace Microsoft.Azure.SignalR
{
internal sealed class ClientInvocationManager : IClientInvocationManager
{
public ICallerClientResultsManager Caller { get; }
public IRoutedClientResultsManager Router { get; }

public ClientInvocationManager(IHubProtocolResolver hubProtocolResolver)
{
Caller = new CallerClientResultsManager(hubProtocolResolver ?? throw new ArgumentNullException(nameof(hubProtocolResolver)));
Router = new RoutedClientResultsManager();
}
}
}
#endif
Loading