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.
Connected.Framework/Connected.Net/Server/ServerProtocol.cs

131 lines
5.4 KiB

2 years ago
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));
}
}