You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
131 lines
5.4 KiB
131 lines
5.4 KiB
using System.Collections.Immutable;
|
|
using Connected.Configuration;
|
|
using Connected.Net.Endpoints;
|
|
|
|
namespace Connected.Net.Server;
|
|
|
|
/// <summary>
|
|
/// This class resolves Endpoints server. There should be one and only one Endpoint server in the environment (network).
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// No instance is preconfigured to be an Endpoint server thus enabling the system to not have a single point of failure. If there is
|
|
/// only one instance in the environment that instance acts as an Endopint server as well. As other instances are created/loaded they will
|
|
/// connect to this Endpoint (server). If this instance goes down or restarts one of the other instances will take the server role ensuring if
|
|
/// we have at least one process active in the environment we have one and only one Endpoint server as well.
|
|
/// </remarks>
|
|
internal class ServerProtocol
|
|
{
|
|
public ServerProtocol(ImmutableList<IEndpoint> endpoints, IEndpointServer server, IConfigurationService configurationService, IHttpService http)
|
|
{
|
|
Endpoints = endpoints;
|
|
ConfigurationService = configurationService;
|
|
Http = http;
|
|
Server = server as EndpointServer;
|
|
}
|
|
private ImmutableList<IEndpoint> Endpoints { get; }
|
|
private EndpointServer Server { get; }
|
|
private IConfigurationService ConfigurationService { get; }
|
|
private IHttpService Http { get; }
|
|
|
|
public async Task<IEndpoint?> ResolveServer(CancellationToken cancellationToken = default)
|
|
{
|
|
/*
|
|
* If there are no endpoints defined this process is the
|
|
* only instance in the environment.
|
|
*/
|
|
if (Endpoints is null || !Endpoints.Any())
|
|
return null;
|
|
/*
|
|
* First try to find existing one.
|
|
*/
|
|
if (await Lookup(Endpoints, cancellationToken) is IEndpoint existing)
|
|
return existing;
|
|
/*
|
|
* Check if we are the only instance in the environment. If so, we are the server of course.
|
|
*/
|
|
if (Endpoints.Count == 1 && string.Equals(Endpoints[0].Address, ConfigurationService.Endpoint.Address, StringComparison.OrdinalIgnoreCase))
|
|
return Endpoints[0];
|
|
/*
|
|
* We are in the scale out environment and things got a bit complicated.
|
|
* No server exist is the environment but we have at least two instances.
|
|
* We must now negotiate and eventually choose a winner which will act
|
|
* as a server.
|
|
* If our proposal fails there must be a better candidate to be a server we just need to
|
|
* look for it again.
|
|
*/
|
|
if (await Propose(Endpoints, cancellationToken) is IEndpoint server)
|
|
return server;
|
|
/*
|
|
* No luck we just hope now that other instance was actually chosen to be a server.
|
|
* If if don't find it now we are in big trouble because we couldn't agree which one
|
|
* are gonna be a server but the platform must have an Endpoint server.
|
|
*/
|
|
if (await Lookup(Endpoints, cancellationToken) is not IEndpoint endpoint)
|
|
throw new SysException(this, SR.ErrCannotResolveEndpoint);
|
|
/*
|
|
* We are lucky and have a server.
|
|
*/
|
|
return endpoint;
|
|
}
|
|
/// <summary>
|
|
/// This method tries to find an existing server in the environment.
|
|
/// </summary>
|
|
/// <param name="endpoints"></param>
|
|
/// <returns>The Endpoint which acts as a server. Null if there is no server.</returns>
|
|
private async Task<IEndpoint?> Lookup(ImmutableList<IEndpoint> endpoints, CancellationToken cancellationToken)
|
|
{
|
|
foreach (var endpoint in endpoints)
|
|
{
|
|
/*
|
|
* Don't connect to itself.
|
|
*/
|
|
if (string.Equals(endpoint.Address, ConfigurationService.Endpoint.Address, StringComparison.OrdinalIgnoreCase))
|
|
continue;
|
|
/*
|
|
* We are done if we found a endpoint server.
|
|
*/
|
|
if (await IsServer(endpoint, cancellationToken))
|
|
return endpoint;
|
|
}
|
|
|
|
return default;
|
|
}
|
|
/// <summary>
|
|
/// Tries to connect to the endpoint and if connection is establied successfully
|
|
/// finds out if the endpoint is already a server.
|
|
/// If so, the negotiation is completed.
|
|
/// </summary>
|
|
private async Task<bool> IsServer(IEndpoint endpoint, CancellationToken cancellationToken = default)
|
|
{
|
|
return await Http.Get<bool>($"{endpoint.Address}/{Routes.EndpointsService}/{nameof(Server.IsServer)}", cancellationToken);
|
|
}
|
|
/// <summary>
|
|
/// Thos method negotiates with other instances and tries to eventually chooses the one which will act
|
|
/// as a server.
|
|
/// </summary>
|
|
/// <returns>The chosen endpoint</returns>
|
|
private async Task<IEndpoint?> Propose(ImmutableList<IEndpoint> endpoints, CancellationToken cancellationToken = default)
|
|
{
|
|
/*
|
|
* The algorithm is as follows:
|
|
* - compare proposal arguments with other instances
|
|
* - if our proposal is the oldest and with the largest value we are the server
|
|
* - otherwise some other instance will act as a server
|
|
*/
|
|
foreach (var endpoint in endpoints)
|
|
{
|
|
if (string.Equals(endpoint.Address, ConfigurationService.Endpoint.Address, StringComparison.OrdinalIgnoreCase))
|
|
continue;
|
|
/*
|
|
* If at least one endpoint returns false it means it has a better proposal to be a server.
|
|
*/
|
|
if (!await Http.Post<bool>($"{endpoint.Address}/{Routes.EndpointsService}/{nameof(Server.Propose)}", EndpointServer.ProposalArgs, cancellationToken))
|
|
return null;
|
|
}
|
|
/*
|
|
* All endpoints agreed we are the best candidate to be a server.
|
|
*/
|
|
return endpoints.First(f => string.Equals(f.Address, ConfigurationService.Endpoint.Address));
|
|
}
|
|
}
|