Skip to content
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

No Available Server Connexion #225

Open
Naowo opened this issue Apr 27, 2023 · 3 comments
Open

No Available Server Connexion #225

Naowo opened this issue Apr 27, 2023 · 3 comments

Comments

@Naowo
Copy link

Naowo commented Apr 27, 2023

Hello,

I'm trying to use the AzureSignalR Service in azure, with my own negociation endpoint, I'm using the sample that can be found here :

I have a client app (angular), wich is configured to connect on my SignalR with the following code : 

this.hubConnection = new HubConnectionBuilder()
        .withUrl(`[https://negotiationserver20230424040158.azurewebsites.net/message`] 
        (https://negociationserver*********.azurewebsites.net/message), signalROptions)
        .build();

"signalROptions" look's like this : 

const signalROptions = {
        logging : LogLevel.Critical,
        accessTokenFactory : () => this.msalService.getTokenForScopes(this.msalService.getScopesForEndpoint(this.appConfig.apiEndpoints.contracts)),
      } as IHttpConnectionOptions

At this point everything works well, the negociation works as it's suppose to work, the app make a POST HTTP Request on my own negociation server, my negociation server is doing the "negociateAsync" (the method exposed by the ServiceHubContext Object), the azure signalR service response to this request with the signalR service url & an access token. My negociation endpoint is responding to my client with this 2 informations, then my Angular App is handling this response and make a new negociate call this time on the azure signalR service, the negociation is passing, no problems there in the app log I see a log wich indicate the web socket connexion is established.

Some secondes later (5 secondes max), I can see the following error push to the client in the websocket : 
Error: Server returned handshake error: SignalR Service is now in 'Default' service mode. Current mode works as a proxy that routes client traffic to the connected app servers. However app servers are not connected.

This exception fired by the azure Signal R service is this one : NoAvailableServerConnection wich close the websocket connection on my client App.

I have tried so many things, hosting the negociation on azure in same region and the same resource group of my azure signalR service, add/remove "addSignalR", "addAzureSignalR" & "MapHub" or put the ServiceTransportType on Persistent mode, but that's does not solve the problem.

So at this point I'm stuck with the error and no solutions to solve it. 
Have you any advises to help me fix it ?

Thank's for your reading. 
Have a nice day.

@chenkennt
Copy link
Contributor

From the error message, you don't have a SignalR server to handle the client connections (see this doc for more details). Could you elaborate a bit more on how your negotiation server works (from what you have described I cannot easily tell)? If your negotiation server just generates url and access token for clients but does not connect to SignalR service, you will see this error in default mode.

Another option is to use the serverless mode of Azure SignalR, which doesn't require a SignalR server, but then you will lose the ability to send messages from client to server (or use a different way called upstream to achieve this). So depending on your use case and architecture, you can choose this option.

@Naowo
Copy link
Author

Naowo commented May 9, 2023

Hi @chenkennt,

Thank's for your answer, and I'm sorry for my response delay but I was on hollidays.

So I'm gonna try to explain how my negociation server works in deep.

I have an SignalRService it's implemented as this :

public class SignalRService : IHostedService, IHubContextStore
    {
        private readonly IConfiguration _configuration;

        public ServiceHubContext MaintenanceHub { get; private set; }

        public SignalRService(IConfiguration configuration)
        {
            this._configuration = configuration;
        }

        public async Task StartAsync(CancellationToken cancellationToken)
        {
            using ServiceManager serviceManager = new ServiceManagerBuilder()
                 //.WithConfiguration(this._configuration)
                 .WithOptions(opt => {
                     opt.ConnectionString = "Endpoint=***********";
                     opt.ServiceTransportType = ServiceTransportType.Persistent;
                 })
                 .BuildServiceManager();


            this.MaintenanceHub = await serviceManager.CreateHubContextAsync("notification", cancellationToken);
        }

        public Task StopAsync(CancellationToken cancellationToken)
        {
            return Task.WhenAll(Dispose(this.MaintenanceHub));
        }

        private static Task Dispose(ServiceHubContext hubContext)
        {
            if(hubContext == null)
            {
                return Task.CompletedTask;
            }
            return hubContext.DisposeAsync();
        }
    }

This service is injected in my server startup on my ConfigureServices :

  services
      .AddSingleton<SignalRService>()
      .AddHostedService(sp => sp.GetService<SignalRService>())
      .AddSingleton<IHubContextStore>(sp => sp.GetService<SignalRService>());

I have a NegociateController :

public class NegociateController : ControllerBase
    {
        private readonly ServiceHubContext _maintenanceHubContext;

        public NegociateController(IHubContextStore store, QueryDatabaseContext dbContext)
        {
            this._maintenanceHubContext = store.MaintenanceHub;
            this._queryDatabaseContext = dbContext;
        }

        [HttpPost("notification/negotiate")]
        [Authorize]
        public Task<ActionResult> MaintenceHubNegociate()
        {
            //this._queryDatabaseContext.UserProfiles.Where(up => up.Email == this.User.);
            string userId = this.User.Claims.FirstOrDefault(clm => clm.Type == "global_id")?.Value;
            return this.NegotiateBase(userId, this._maintenanceHubContext);
        }

        private async Task<ActionResult> NegotiateBase(string user, ServiceHubContext serviceHubContext)
        {
            if (string.IsNullOrEmpty(user))
            {
                return BadRequest("User ID is null or empty.");
            }
            else
            {
                Console.WriteLine(user);
            }

            var negotiateResponse = await serviceHubContext.NegotiateAsync(new()
            {
                UserId = user,
                EnableDetailedErrors = true
            });

            return new JsonResult(new Dictionary<string, string>()
            {
                { "url", negotiateResponse.Url },
                { "accessToken", negotiateResponse.AccessToken }
            });
        }
  }

With this code, the negociation works well, the AzureSignalR url is returned by the negociation endpoint & the associated access token.
My FrontEnd App handle this response and established a connection with the azure signalR
In the Web Socket I see the error I give abrove.

Then I told my self the problem is the connection between the NegociationServier and the AzureSignalR.
So I've tried to "Add" the azure signalR to my negociation server I've add this code to my startup :

services
                .AddSignalR();
                .AddAzureSignalR(options =>
                {
                    options.Endpoints = new ServiceEndpoint[]
                    {
                            new ServiceEndpoint("Endpoint=********")
                    };
                });

And I still have the same error.

At this point I've tried a last thing, add the hub endpoint in my negociation server configure :

endpoints.MapHub<SignalR.UpdateNotificationHub>("/notification");

for your information I put the code of my Hub here :

public class UpdateNotificationHub : Hub
    {
        /// <summary>
        /// Backend update start.
        /// </summary>
        /// <param name="payload">The payload.</param>
        public async Task BackendUpdateStart(MaintenanceMessage payload)
        {
            payload.Types = MessageTypesEnum.BackendUpdating;
            payload.MetaData.ClientsNotifiedAt = DateTime.UtcNow;

            await this.Clients.AllExcept(this.Context.ConnectionId).SendAsync("backend-update", payload);
            payload.MetaData.ExecutionFinishedAt = DateTime.UtcNow;
        }

        /// <summary>
        /// Backend update end.
        /// </summary>
        /// <param name="payload">The payload.</param>
        public async Task BackendUpdateEnd(MaintenanceMessage payload)
        {
            payload.Types = MessageTypesEnum.BackendUpdating;
            payload.MetaData.ClientsNotifiedAt = DateTime.UtcNow;

            await this.Clients.AllExcept(this.Context.ConnectionId).SendAsync("backend-update", payload);
            payload.MetaData.ExecutionFinishedAt = DateTime.UtcNow;
        }

        /// <summary>
        /// Frontends the update start.
        /// </summary>
        /// <param name="payload">The payload.</param>
        public async Task FrontendUpdateStart(MaintenanceMessage payload)
        {
            payload.Types = MessageTypesEnum.FrontendUpdating;
            payload.MetaData.ClientsNotifiedAt = DateTime.UtcNow;

            await this.Clients.AllExcept(this.Context.ConnectionId).SendAsync("frontend-update", payload);
            payload.MetaData.ExecutionFinishedAt = DateTime.UtcNow;
        }

        /// <summary>
        /// Frontends the update end.
        /// </summary>
        /// <param name="payload">The payload.</param>
        public async Task FrontendUpdateEnd(MaintenanceMessage payload)
        {
            payload.Types = MessageTypesEnum.FrontendUpdating;
            payload.MetaData.ClientsNotifiedAt = DateTime.UtcNow;

            await this.Clients.AllExcept(this.Context.ConnectionId).SendAsync("frontend-update", payload);
            payload.MetaData.ExecutionFinishedAt = DateTime.UtcNow;
        }
    }

I have already seen this documention you linked.
I understand, that I need a connection between my NegociationServer and the AzureSignalR but what can I do more that what I've already done to make this connection work.

There is somethig I missed in my startup or my service ?

Thank's for your time, I'm quite lost about this connection between my negociation server and AzureSignalR.

@chenkennt
Copy link
Contributor

Let me explain this a bit more.

Azure SignalR Service can work in two different ways:

  1. If your server is a typical ASP.NET web application, you can create a hub in your server and call AddAzureSignalR() to connect your server with the service. In this case you don't need to implement your negotiate API (controller), SignalR SDK will automatically add a negotiate endpoint based on the mapHub() you call. So the minimum code you need to write is like this:
    class YourHub: Hub
    {
        //...
    }
    
    services.AddSignalR().AddAzureSignalR();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapHub<YourHub>("<hub_path>");
    });
  2. If you don't want to create a hub (for example you don't have the need to be able to call hub methods from clients, or your server is not an ASP.NET web application, like Azure Functions), you can set the service mode to "serverless" (in Azure portal). Then SignalR service no longer requires a server to be connected. But in this case, you will need to implement the negotiate API by yourself (like what you have already done in the NegociateController). Also you'll need to use a different interface (ServiceManager) to send messages from server to client, and won't be able to use Hub to receive messages from clients (unless you use a different feature called upstream).

From what you have described seems the first option is a better choice for you. You can refer to this project which contains a minimum sample about how to establish bidirectional communication between sever and clients. Keep in mind in this mode you don't need to implement the negotiate by yourself and use the ServiceManager class. Everything can be done through the native SignalR interface.

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

No branches or pull requests

2 participants