Scale out SignalR: Sending messages locally and through a backplane

24 Feb, 2017 | 4 minutes read

Scale-out is one of the key things to think about when designing your application. SignalR has a mechanism for handling scale-out by forwarding messages among servers through a backplane. When a message is sent, it goes through the backplane. There are no exceptions, every message has to pass through the backplane. So, the backplane can become a bottleneck. The solution is to send the messages locally to the users that are connected to the same server and use the backplane only for communicating with users that are connected to a different server. We’ll take a chat application as an example.

Use of the SignalR instance on Server A and Server B

The idea is to use the SignalR instance on Server A and Server B as a client to a separate SignalR server (in this example it is a self-hosted application) that will use Redis backplane. You need to create a custom message bus that will be used to replace the default message bus in the application that is hosted on servers A and B.  If the recipient is not connected on the same server, the message details will be sent to the SignalR server with the Redis backplane, and from there the message details will be forwarded to the recipient where in the custom message bus the message will be rebuilt from the message details. If the recipient is connected to the same server, the message won’t be forwarded to the SignalR server with Redis backplane.

Create a CustomBackplane class that inherits from the MessageBus class. In our case, we can’t use the ScaleoutMessageBus because it requires a configuration object.  Since we are using the custom message bus only to separate the messages that need to be sent locally from those that need to be sent to another server instance, we have no use of the configuration object.

In the custom message bus add the following fields and a URL pointing to the SignalR instance with a Redis backplane.

private IHubProxy HubProxy { get; set; }
private string ServerURI = "http://localhost:50578/signalr";
private HubConnection Connection { get; set; }

In the constructor create a new hub connection and a hub proxy.

Connection = new HubConnection(ServerURI);
HubProxy = Connection.CreateHubProxy("MyHub");

Next, we initiate the connection.

try
{
     Connection.Start().Wait();
}
catch (Exception ex)
{
     Debug.WriteLine("Error on subscribe to Redis" + ex);
}

The key part is to override the publishing logic.  We can get the UserId and ConnectionId values from the key. It’s used in the format “hu-“ for hub UserId and “hc-“ for a hub ConnectionId followed by a period character.

public override Task Publish(Message message)
{     
      string strKey = message.Key;
      string strConnectionId = string.Empty;
      string recipientUserId = string.Empty;
      int dotIndex = strKey.IndexOf('.');
            
      if (dotIndex > -1 && strKey.StartsWith("hu-"))
      {
            recipientUserId = strKey.Substring(strKey.IndexOf('.') + 1);
      }

      if (dotIndex > -1 && strKey.StartsWith("hc-"))
      {
            strConnectionId = strKey.Substring(strKey.IndexOf('.') + 1);
      }

      if (!UserHandler.IsUserConected(recipientUserId) && strConnectionId == string.Empty)
      {
            HubProxy.Invoke("Send", message.GetString(), message.Key, message.Source, message.CommandId, message.IsAck, message.WaitForAck);

            var taskCompletionSource = new TaskCompletionSource<object>();
            taskCompletionSource.TrySetResult(null);

            return taskCompletionSource.Task;
      }

      return base.Publish(message);
}

On the Hub in the SignalR server with Redis backplane define a method Send that will receive the message details and call a method SendMessage on the client (it’s a method on the hub proxy that we created in the custom message bus).

public void Send(string message, string key, string source, string commandId, bool isAck, bool waitForAck)
{
       Clients.All.sendMessage(message, key, source, commandId, isAck, waitForAck);
}

Next register to the SendMessage event in the custom message bus constructor and specify a callback where the message details are used to build the message object and publish it locally.

HubProxy.On<string, string, string, string, bool, bool>("SendMessage", (message, key, source, commandId, isAck, waitForAck) =>
    {
           Message objMessage = new Message();
           objMessage.Key = key;
           objMessage.Source = source;
           objMessage.Value = new ArraySegment<byte>(Encoding.UTF8.GetBytes(message));
           objMessage.CommandId = commandId;
           objMessage.IsAck = isAck;
           objMessage.WaitForAck = waitForAck;
           base.Publish(objMessage);
    }
);

Conclusion

This solution allows the application to scale as the rate of messages grows proportionally with the number of chat users. Only a subset of messages would be forwarded to the backplane, so it wouldn’t become a bottleneck. It also decreases resource usage, whether you are using resources on-premises or in the cloud. Also, messages will be delivered faster since the messages won’t travel to the backplane if not necessary, and the backplane won’t be overloaded with messages without a need.

This way, we have used the backplane mechanism as part of the bigger picture. It allowed us to handle a high-traffic scenario at high peeks where a large number of users are sending an even larger number of messages.