Skip to content

Conversation

@xingsy97
Copy link
Contributor

@xingsy97 xingsy97 commented Sep 20, 2022

Summary of the changes

  • Implement ClientResultsManager

See #1687 for how to use ClientInvocationManager


public void TryCompleteRoutedResult(string connectionId, CompletionMessage message);

public (Type Type, string ConnectionId, object Tcs, Action<object, CompletionMessage> Completion)? RemoveInvocation(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.

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?

Copy link
Contributor Author

@xingsy97 xingsy97 Sep 25, 2022

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 serviceMappingMessage is 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
    • AddServiceMappingMessage doesn't involve in this procedure.
  • 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 memebers Caller and Router. The Caller manages all client invocations which are originally created by the server. While the Router manages 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);
Copy link
Member

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

}
if (_routedInvocations.TryGetValue(invocationId, out var item2))
{
type = item2.Type;
Copy link
Contributor

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?

Copy link
Contributor Author

@xingsy97 xingsy97 Sep 29, 2022

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)

@xingsy97
Copy link
Contributor Author

See #1687 for how to use ClientInvocationManager


bool TryGetInvocationReturnType(string invocationId, out Type type);

void CleanupInvocations(string instanceId);
Copy link
Contributor

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);
Copy link
Member

Choose a reason for hiding this comment

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

naming: UpdateServiceMapping?

Copy link
Member

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?

Copy link
Contributor Author

@xingsy97 xingsy97 Oct 10, 2022

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.AddOrUpdate is used to avoid manual checking whether a key exists 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. CleanupInvocations cleans _serviceMappingMessages only when a service is offline. _serviceMappingMessages can be cleaned when an invocation is completed normally as well.


bool TryGetInvocationReturnType(string invocationId, out Type type);

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.

_serviceMappingMessages.AddOrUpdate(
serviceMappingMessage.InstanceId,
new List<string>() { serviceMappingMessage.InvocationId },
(key, valueList) => { valueList.Add(serviceMappingMessage.InvocationId); return valueList; });
Copy link
Member

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

Copy link
Contributor Author

@xingsy97 xingsy97 Oct 10, 2022

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)

{
tcs.SetResult((T)completionMessage.Result);
invocation.RouterInstanceId = serviceMappingMessage.InstanceId;
_pendingInvocations[serviceMappingMessage.InvocationId] = invocation;
Copy link
Member

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.");
Copy link
Member

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)))
Copy link
Member

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

}
else
{
throw new InvalidOperationException($"Failed to record a service mapping whose RouterInstanceId '{serviceMappingMessage.InvocationId}' was already existing.");
Copy link
Member

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;
Copy link
Member

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)))
Copy link
Member

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; }
Copy link
Member

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 {
Copy link
Member

Choose a reason for hiding this comment

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

  1. Any other classes consuming this PendingInvocation? If yes, move to a separate file, if not, move into CallerClientResultManager class and make it private
  2. you can simplify the code as below since only RouterInstanceId is settable
    record 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
Copy link
Member

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)
Copy link
Member

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
{

Copy link
Member

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();
Copy link
Member

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;
Copy link
Member

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;
Copy link
Member

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);
Copy link
Member

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.");
Copy link
Member

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.");
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?

var protocol = _hubProtocolResolver.GetProtocol(message.Protocol, null);
if (protocol == null)
{
throw new InvalidOperationException($"Not supported protocol {message.Protocol} by server");
Copy link
Member

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);
Copy link
Member

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;
Copy link
Member

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)
Copy link
Member

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)
Copy link
Member

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?

@xingsy97
Copy link
Contributor Author

xingsy97 commented Oct 13, 2022

I caused a wrong merge accidentally. Many commits from PR 1687 were merged in.
Now I reverted the wrong merge. So just ignore

}
else
{
var errorMessage = $"The payload of ClientCompletionMessage whose type is {hubMessage.GetType().Name} cannot be parsed into CompletionMessage correctly.";
Copy link
Member

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);
Copy link
Member

Choose a reason for hiding this comment

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

remove?

@xingsy97
Copy link
Contributor Author

xingsy97 commented Oct 14, 2022

Now RoutedClientResultsManager will not record InstanceId. And its Cleanup is according to ConnectionId rather than InstanceId

@xingsy97 xingsy97 merged commit 724624c into Azure:dev Oct 14, 2022
@xingsy97 xingsy97 deleted the ci-ClientResultsManager branch October 14, 2022 06:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants