question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

The SignalR service SDK should be a de-multiplexer that plugs at the HubConnectionHandler

See original GitHub issue

This is basically an uber issue caller for the re-design of the how the integration works. Today there’s too much copied code from SignalR which is fragile and ever changing. At an extremely high level, the SignalR SDK is handling incoming messages from the SignalR service and pumping those messages into individual connections as they are received. It should be no different from those connection being directly connected to the application conceptually, the only different being that there’s no physical connection, it’s virtual (being de-multiplexed from the service).

What this means, is that there are 2 major pieces that need to be replaced:

  1. The IConnectionBuilder - SignalR binds to any IConnectionBuilder that can be provided, by default that’s one provided by Microsoft.AspNetCore.Http.Connections, which provides this abstraction over WebSockets, SeverSentEvents and LongPolling.
  2. The HubLifetimeManager - This handles messages send from server to client. We have a built in one for in memory (DefaultHubLifetimeManager) and one for redis (RedisHubLifetimeManager).

For 1. the service needs to replace the built in IConnectionBuilder with one that handles incoming traffic from the service. This is basically what HubHost<THub> does (see https://github.com/Azure/azure-signalr/blob/071ad301a934001c872ef417812ed8ee78a1fc3a/src/Microsoft.Azure.SignalR/HubHost/HubHostBuilder.cs#L26-L29).

For 2. the service needs to replace the DefaultHubLifetimeManager with one that sends server to client “commands” back to the service itself. This is what the HostHubLifetimeManager does (https://github.com/Azure/azure-signalr/blob/071ad301a934001c872ef417812ed8ee78a1fc3a/src/Microsoft.Azure.SignalR/HubHost/HubHostLifetimeManager.cs)

Everything should revolve around these 2 layers but today the SDK dives a bit deeper into the SignalR logic which it shouldn’t (see the https://github.com/Azure/azure-signalr/blob/75efc45046d61878c354992bfbc6ab3dff52213d/src/Microsoft.Azure.SignalR/HubHost/HubHostDispatcher.cs). This logic should be the default SignalR logic and the only thing we should be replacing is the transport layer (how bytes get into and out of the application).

Microsoft.Azure.SignalR.Connections

If you look closely at the above, it suggests that in essence the service is itself a transport layer as it completely replaces Microsoft.AspNetCore.Http.Connections. The way I envision this working is that we have a type called ServiceConnectionManager that manages the CloudConnection (maybe rename to ServiceConnection?) that talk to the SignalR service. This is this has 2 responsibilities, it handles the incoming traffic from the service, decodes the messages type appropriately, creates a ConnectionContext with the appropriate set of features and writes the message payload to the Application’s input pipe. It also needs to read messages coming out of the application’s pipe and write those to the ServiceConnection. This is all hidden from the SignalR layer, nothing on top would need to change, there would be no custom HubDispatcher or any other extensibility.

For the ServiceHubLifetimeManager, it would directly get access to the ServiceConnectionManager to send special messages to the service for those message types. This is very similar to redis except that it’s much easier since it doesn’t need to do any of the receiving of messages.

If you want, I can throw something together quickly to demonstrate how this should work. Let me know.

Issue Analytics

  • State:closed
  • Created 5 years ago
  • Comments:5 (5 by maintainers)

github_iconTop GitHub Comments

1reaction
davidfowlcommented, Apr 4, 2018

I decided to throw together some pseudo code. Tell me if I’ve missed anything.

// Create a new connection builder
var connectionBuilder = new ConnectionBuilder();

// Plug in a SignalR Hub
connectionBuilder.UseHub<Chat>();

// Create the application delegate
ConnectionDelegate app = connectionBuilder.Build();

// This is a connection to the SignalR service (we have many of these I think 4 by default)
var serviceConnection = new HttpConnection(...);
var serviceConnectionLock = new SemaphoreSlim(1, 1);

// This is a map of connection id to ServiceConnectionContext. These map the original connection ids
// from the client side to server side objects that we can manipulate
ConcurrentDictionary<string, ServiceConnectionContext> _connections = new ConcurrentDictionary<string, ServiceConnectionContext>();

public class ServiceProtocol
{
    // Handle parsing and writing ServiceMessage messages
     bool TryParseMessage(ref ReadOnlySequence<byte> buffer, out ServiceMessage message);
     void WriteMessage(ServiceMessage message, IBufferWriter<byte> bufferWriter);
}

private class ServiceMessage
{
    // Protocol between SDK and Service 
}

// This is the service implementation of a ConnectionContext, it takes the OnConnectedAsync message
// and creates a connection from it with the right metadata.
public class ServiceConnectionContext : ConnectionContext
{
    public ServiceConnectionContext(ServiceMessage message) 
    { 
    }
}

// This method handle de-multiplexing messages from the service and dispatches to the appropriate
// connnection
private async Task ProcessIncomingAsync(HttpConnection serviceConnection)
{
    while (true)
    {
        var result = await serviceConnection.Input.ReadAsync();
        var buffer = result.Buffer;

        if (!buffer.IsEmpty)
        {
            while (ServiceProtocol.TryParseMessage(ref buffer, out ServiceMessage message))
            {
                switch (message.InvocationType)
                {
                    case ServiceMessageType.OnConnected:
                        // Create the ServiceConnectionContext from the OnConnected message. This should set the ConnectionId, claims and other things that originally came in from the client
                        var connection = new ServiceConnectionContext(message);
                        _connections[message.ConnectionId] = connection;


                        // Start processing all outgoing messages for this connection
                        _ = ProcessOutputMessagesAsync(serviceConnection, connection);

                       // Execute the application code, this will call into the SignalR end point
                       _ = app(connection);

                      // REVIEW: We probably want to keep track of these tasks so we can complete the pipes if they weren't already completed. The SignalR handler will be updated appropriately.

                        break;
                    case ServiceMessageType.OnDisconnected:
                        _connections.TryRemove(out message.ConnectionId, out var connection);
                        // Close this connection gracefully then remove it from the list, this will trigger the hub shutdown logic appropriately
                        _ = connection.CloseAsync();
                        break;
                    default:
                        if (_connections.TryGetValue(message.ConnectionId, out var connection))
                        {
                            // Write the raw connection payload to the pipe let the upstream handle it
                            await connection.Application.Output.WriteAsync(message.Payload);
                        }
                        else
                        {
                            // Log what happened here
                        }
                        break;
                }
            }
        }
        else if(result.IsCompleted)
        {
            // The connection is closed (reconnect)
            break;
        }

        serviceConnection.Input.Advance(buffer.Start, buffer.End);
    }
} 


private async Task ProcessOutgoing(HttpConnection serviceConnection, ServiceConnectionContext connection)
{
    while (true)
    {
        var result = await connection.Application.Input.ReadAsync();
        var buffer = result.Buffer;

        if (!buffer.IsEmpty)
        {
            // We need to wrap the buffer in a outgoing frame with the original connection id before sending
            // it to the service
            var serviceMessage = new ServiceMessage 
            {
                ConnectionId = connection.ConnectionId,
                Payload = buffer.ToArray() // This could be more efficient if we wrote the format directly to the PipeWriter
            };

            try 
            {
                // We have to lock around outgoing sends since the pipe is single writer.
                // The lock is per serviceConnection
                await serviceConnectionLock.WaitAsync();

                // Write the service protocol message
                ServiceProtocol.Write(serviceMessage, serviceConnection.Output);

                await serviceConnection.Output.FlushAsync();                
            }
            finally
            {
                serviceConnectionLock.Release();
            }

        }
        else if (result.IsCompleted)
        {
            // This connection ended (the application itself shut down) we should remove it from the list of connections
            break;
        }
    }

    // We should probably notify the service here that the application chose to abort the connection
    _connections.TryRemove(connection.ConnectionId, out _);
} 
0reactions
davidfowlcommented, Apr 23, 2018

This is fixed

Read more comments on GitHub >

github_iconTop Results From Across the Web

Service mode in Azure SignalR Service
An overview of service modes in Azure SignalR Service. ... In Default mode you can also use REST API, management SDK, and function...
Read more >
Azure Functions and SignalR with Anthony Chu - YouTube
Using Azure Functions and Azure SignalR Service, you can serverlessly broadcast ... .com/learn/modules/publish-app-service-static-web-app- api /?
Read more >
How YOU can learn to build real-time Web Apps that ...
We will: Provision an Azure SignalR service; Create an Azure Function app, that will allow us to connect to the Azure SignalR Service....
Read more >
Unable to connect to Azure SignalR service after deployment
Firstly, I extended( as error says ) this rule in Azure Front Door: FROM: "default-src 'self' *.my.com graph.microsoft.com". TO:
Read more >
SignalR deep dive: Key concepts, use cases, and limitations
A SignalR hub is a high-level pipeline that enables connected servers and clients to define and invoke methods on each other, thus facilitating ......
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found