-
Notifications
You must be signed in to change notification settings - Fork 108
Add ClientResultsManager #1684
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
Add ClientResultsManager #1684
Conversation
src/Microsoft.Azure.SignalR.Common/ClientInvocation/ClientResultsManager.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Azure.SignalR.Common/ClientInvocation/ClientResultsManager.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Azure.SignalR.Common/ClientInvocation/ClientResultsManager.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Azure.SignalR.Common/ClientInvocation/ClientResultsManager.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Azure.SignalR.Common/ClientInvocation/ClientResultsManager.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Azure.SignalR.Common/ClientInvocation/IClientResultsManager.cs
Show resolved
Hide resolved
src/Microsoft.Azure.SignalR.Common/ClientInvocation/ClientResultsManager.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Azure.SignalR.Common/ClientInvocation/IClientResultsManager.cs
Outdated
Show resolved
Hide resolved
|
|
||
| public void TryCompleteRoutedResult(string connectionId, CompletionMessage message); | ||
|
|
||
| public (Type Type, string ConnectionId, object Tcs, Action<object, CompletionMessage> Completion)? RemoveInvocation(string invocationId); |
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.
From an interface design perspective, the method naming needs to be readable and self explainable.
- How is serviceMappingMessage related to offline ping? Why AddServiceMappingMessage does not withOfflinePing?
- what is invocation and what is routedInvocation? why one has Add one does not? the difference is one has callServerId and one does not?
- Is that the caller knows which is routed which is not? could it be the ClientResultManager to handle the logic? Will that make the logic simpler?
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.
- When a pod will go offline, all client invocations which were routed by this pod should be cleaned. These invocations are stored in their route server and original caller server.
- That's why
serviceMappingMessageis related to offline ping. - After a server receives an offline ping from a specific pod, this server should do 2 things
- For all invocations called by it, clean those were routed by the pod
- For all invocations routed by it, clean those were routed by the pod
AddServiceMappingMessagedoesn't involve in this procedure.
- That's why
- The difference between invocation and routed invocation is whether the server is the original caller server for the invocation.
- I designed a class
ClientResultsManager. It has 2 memebersCallerandRouter. TheCallermanages all client invocations which are originally created by the server. While theRoutermanages all invocations which are routed by the server.
|
|
||
| public Task<T> AddInvocation<T>(string connectionId, string invocationId, CancellationToken cancellationToken) | ||
| { | ||
| var tcs = new TaskCompletionSourceWithCancellation<T>(this, connectionId, invocationId, cancellationToken); |
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.
is canellationToken.Register(()=>tcs.SetCancelled()) enough?
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.
See this comment
src/Microsoft.Azure.SignalR.Common/ClientInvocation/ClientResultsManager.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Azure.SignalR.Common/ClientInvocation/IClientResultsManager.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Azure.SignalR.Common/ClientInvocation/ClientResultsManager.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Azure.SignalR.Common/ClientInvocation/IClientResultsManager.cs
Outdated
Show resolved
Hide resolved
| } | ||
| if (_routedInvocations.TryGetValue(invocationId, out var item2)) | ||
| { | ||
| type = item2.Type; |
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.
For routed ones, it seems always return object refer to the add method. Simply make type null and return true?
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.
type will return typeof(RawResult) now.
See details in #1684 (comment) and #1684 (comment)
src/Microsoft.Azure.SignalR.Common/ClientInvocation/ClientResultsManager.cs
Outdated
Show resolved
Hide resolved
…/azure-signalr into ci-ClientResultsManager
…/azure-signalr into ci-ClientResultsManager
|
See #1687 for how to use |
src/Microsoft.Azure.SignalR.Common/ClientInvocation/TaskCompletionSourceWithCancellation.cs
Show resolved
Hide resolved
src/Microsoft.Azure.SignalR.Common/ClientInvocation/ClientResultsManager.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Azure.SignalR.Common/ClientInvocation/ClientResultsManager.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Azure.SignalR.Common/ClientInvocation/ClientResultsManager.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Azure.SignalR.Common/ClientInvocation/TaskCompletionSourceWithCancellation.cs
Show resolved
Hide resolved
src/Microsoft.Azure.SignalR.Common/ClientInvocation/ClientResultsManager.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Azure.SignalR.Common/ClientInvocation/IClientResultsManager.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Azure.SignalR.Common/ClientInvocation/IRoutedClientResultsManager.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Azure.SignalR.Common/ClientInvocation/IClientResultsManager.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Azure.SignalR.Common/ClientInvocation/IClientResultsManager.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Azure.SignalR.Common/ClientInvocation/IClientResultsManager.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Azure.SignalR.Common/Microsoft.Azure.SignalR.Common.csproj
Outdated
Show resolved
Hide resolved
src/Microsoft.Azure.SignalR.Common/ClientInvocation/TaskCompletionSourceWithCancellation.cs
Outdated
Show resolved
Hide resolved
|
|
||
| bool TryGetInvocationReturnType(string invocationId, out Type type); | ||
|
|
||
| void CleanupInvocations(string instanceId); |
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.
Routed server has client connection status. This method is not needed, or we may leverage TryCompleteResult() to simply cleanup by connectionId regarding route server finds client disconnect?
| /// <returns></returns> | ||
| Task<T> AddInvocation<T>(string connectionId, string invocationId, string instanceId, CancellationToken cancellationToken); | ||
|
|
||
| void AddServiceMappingMessage(ServiceMappingMessage serviceMappingMessage); |
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.
naming: UpdateServiceMapping?
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.
is this one and the CleanupInvocations one a pair? Give this pair matching names?
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.
- Though this method uses
Dictionary.AddOrUpdate, it's only designed to add a item to a list rather than entirely update a(key, value)according to our business logic. Dictionary.AddOrUpdateis used to avoid manual checking whether akeyexists in the dictionary or not.
_serviceMappingMessages.AddOrUpdate(
serviceMappingMessage.InstanceId,
new List<string>() { serviceMappingMessage.InvocationId },
(key, valueList) => { valueList.Add(serviceMappingMessage.InvocationId); return valueList; });- So I think the old naming is better
- They are not pair or symmetric.
CleanupInvocationscleans_serviceMappingMessagesonly when a service is offline._serviceMappingMessagescan be cleaned when an invocation is completed normally as well.
|
|
||
| bool TryGetInvocationReturnType(string invocationId, out Type type); | ||
|
|
||
| bool ContainsInvocation(string invocationId); |
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.
This method is no need? you can directly use TryGetInvocationReturnType to check if it contains Invocation?
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.
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.
src/Microsoft.Azure.SignalR.Common/ClientInvocation/IRoutedClientResultsManager.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Azure.SignalR/ClientInvocation/CallerClientResultsManager.cs
Outdated
Show resolved
Hide resolved
| _serviceMappingMessages.AddOrUpdate( | ||
| serviceMappingMessage.InstanceId, | ||
| new List<string>() { serviceMappingMessage.InvocationId }, | ||
| (key, valueList) => { valueList.Add(serviceMappingMessage.InvocationId); return valueList; }); |
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.
this valueList.Add is not thread safe
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.
I replace List<string> with ConcurrentBag<string>
Now private readonly ConcurrentDictionary<string, ConcurrentBag<string>> _serviceMappingMessages = new();
I created a new Interface IBaseClientResultsManager and a new class BaseClientResultsManager.
In BaseClientResultsManager, a re-designed _serviceMappingMessages without using List
protected readonly ConcurrentDictionary<string, ConcurrentDictionary<string, bool>> _serviceMapping = new();and public void AddServiceMappingMessage(ServiceMappingMessage serviceMappingMessage)
src/Microsoft.Azure.SignalR/ClientInvocation/CallerClientResultsManager.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Azure.SignalR/ClientInvocation/RoutedClientResultsManager.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Azure.SignalR/ClientInvocation/TaskCompletionSourceWithCancellation.cs
Outdated
Show resolved
Hide resolved
…/azure-signalr into ci-ClientResultsManager
| { | ||
| tcs.SetResult((T)completionMessage.Result); | ||
| invocation.RouterInstanceId = serviceMappingMessage.InstanceId; | ||
| _pendingInvocations[serviceMappingMessage.InvocationId] = invocation; |
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.
what if this invocationId was just removed from _pendingInvocations? Change PendingInvocation to be a reference type?
| else | ||
| { | ||
| tcs.SetException(new Exception(completionMessage.Error)); | ||
| throw new InvalidOperationException($"Failed to record a service mapping whose RouterInstanceId '{serviceMappingMessage.InvocationId}' was already existing."); |
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.
silent failure is enough?
| public void CleanupInvocations(string instanceId) | ||
| { | ||
| if (_serviceMapping.TryRemove(instanceId, out var invocationIdDict)) | ||
| foreach (var (invocationId, invocation) in _pendingInvocations.Select(x => (x.Key, x.Value))) |
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.
why Select? foreach( var (id, invocation) in _pendingInvocations
src/Microsoft.Azure.SignalR/ClientInvocation/CallerClientResultsManager.cs
Show resolved
Hide resolved
| } | ||
| else | ||
| { | ||
| throw new InvalidOperationException($"Failed to record a service mapping whose RouterInstanceId '{serviceMappingMessage.InvocationId}' was already existing."); |
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.
is silent ignore enough?
| if (_routedInvocations.TryGetValue(invocationId, out var invocation)) | ||
| { | ||
| invocation.RouterInstanceId = null; | ||
| _routedInvocations[invocationId] = invocation; |
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.
also here, is reference type better?
|
|
||
| public void CleanupInvocations(string instanceId) | ||
| { | ||
| foreach (var (invocationId, invocation) in _routedInvocations.Select(x => (x.Key, x.Value))) |
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.
also here, why Select?
| { | ||
| internal sealed class DummyClientInvocationManager : IClientInvocationManager | ||
| { | ||
| public ICallerClientResultsManager Caller { get; } |
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.
defensive code as: Caller => throw new NotSupportedException()
|
|
||
| internal record struct PendingInvocation(Type Type, string ConnectionId, string RouterInstanceId, object Tcs, Action<object, CompletionMessage> Complete) | ||
| { | ||
| class PendingInvocation { |
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.
- Any other classes consuming this PendingInvocation? If yes, move to a separate file, if not, move into CallerClientResultManager class and make it private
- you can simplify the code as below since only
RouterInstanceIdis settablerecord PendingInvocation(Type Type, string ConnectionId, object Tcs, Action<object, CompletionMessage> Complete) { public string RouterInstanceId {get; set;} }
| } | ||
|
|
||
| internal record struct RoutedInvocation(string ConnectionId, string CallerServerId, string RouterInstanceId) | ||
| class RoutedInvocation |
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.
similar comment here as PendingInvocation
|
|
||
| class PendingInvocation { | ||
| public PendingInvocation(Type type, string connectionId, string routerInstanceId, object tcs, Action<object, CompletionMessage> complete) | ||
| private record PendingInvocation(Type Type, string ConnectionId, string RouterInstanceId, object Tcs, Action<object, CompletionMessage> Complete) |
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.
remove string RouterInstanceId from ctor
|
|
||
| namespace Microsoft.Azure.SignalR | ||
| { | ||
|
|
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.
remove extra line
| internal sealed class CallerClientResultsManager : ICallerClientResultsManager, IInvocationBinder | ||
| { | ||
| private readonly ConcurrentDictionary<string, PendingInvocation> _pendingInvocations = new(); | ||
| private readonly string _clientResultManagerId = Guid.NewGuid().ToString(); |
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.
use ToString("N")?
| { | ||
| if (invocation.RouterInstanceId == null) | ||
| { | ||
| _pendingInvocations[serviceMappingMessage.InvocationId].RouterInstanceId = serviceMappingMessage.InstanceId; |
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.
use invocation.RouterInstanceId instead of another fetch
| { | ||
| if (_pendingInvocations.TryGetValue(invocationId, out var invocation)) | ||
| { | ||
| _pendingInvocations[invocationId].RouterInstanceId = null; |
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.
When will be the scenario reset the instanceId of the invocation?
| if (_pendingInvocations.TryRemove(message.InvocationId, out _)) | ||
| { | ||
| item.Complete(item.Tcs, message); | ||
| RemoveServiceMapping(message.InvocationId); |
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.
the invocation is already removed
| } | ||
| else | ||
| { | ||
| throw new InvalidOperationException($"The payload of ClientCompletionMessage whose type is {hubMessage.GetType()} cannot be parsed into CompletionMessage correctly."); |
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.
GetType().Name
| { | ||
| return type; | ||
| } | ||
| throw new InvalidOperationException($"Invocation ID '{invocationId}' is not associated with a pending client 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.
Is it possible that invocation is already removed? What happens to the customer when it throws?
src/Microsoft.Azure.SignalR/ClientInvocation/CallerClientResultsManager.cs
Outdated
Show resolved
Hide resolved
| var protocol = _hubProtocolResolver.GetProtocol(message.Protocol, null); | ||
| if (protocol == null) | ||
| { | ||
| throw new InvalidOperationException($"Not supported protocol {message.Protocol} by server"); |
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.
what is the customer experience when it happens?
|
|
||
| public ClientInvocationManager(IHubProtocolResolver hubProtocolResolver) | ||
| { | ||
| Caller = new CallerClientResultsManager(hubProtocolResolver); |
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.
hubProtocolResolver null check
|
|
||
| public CallerClientResultsManager(IHubProtocolResolver hubProtocolResolver) | ||
| { | ||
| _hubProtocolResolver = hubProtocolResolver; |
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.
hubProtocolResolver null check:
_hubProtocolResolver = hubProtocolResolver ?? throw new ArgumentNullException(nameof(hubProtocolResolver))| } | ||
| } | ||
|
|
||
| public void RemoveServiceMapping(string invocationId) |
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.
Can remove?
| { | ||
| Debug.Assert(false); | ||
| } | ||
| public static new void TrySetCanceled(CancellationToken cancellationToken) |
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.
we don't have a void TrySetCanceled?
|
I caused a wrong merge accidentally. Many commits from PR 1687 were merged in. |
| } | ||
| else | ||
| { | ||
| var errorMessage = $"The payload of ClientCompletionMessage whose type is {hubMessage.GetType().Name} cannot be parsed into CompletionMessage correctly."; |
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.
this looks like a system error and should not happen, customer can do nothing about it. For this case, shall we throw?
|
|
||
| public static new void SetException(IEnumerable<Exception> exceptions) => Debug.Assert(false); | ||
| public static new void SetCanceled() => Debug.Assert(false); | ||
| public static new void TrySetCanceled(CancellationToken cancellationToken) => Debug.Assert(false); |
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.
remove?
|
Now |
Summary of the changes
ClientResultsManagerSee #1687 for how to use ClientInvocationManager