using System.Collections.Immutable; using Connected.Configuration; using Connected.Net.Endpoints; namespace Connected.Net.Server; /// /// This class resolves Endpoints server. There should be one and only one Endpoint server in the environment (network). /// /// /// 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. /// internal class ServerProtocol { public ServerProtocol(ImmutableList endpoints, IEndpointServer server, IConfigurationService configurationService, IHttpService http) { Endpoints = endpoints; ConfigurationService = configurationService; Http = http; Server = server as EndpointServer; } private ImmutableList Endpoints { get; } private EndpointServer Server { get; } private IConfigurationService ConfigurationService { get; } private IHttpService Http { get; } public async Task 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; } /// /// This method tries to find an existing server in the environment. /// /// /// The Endpoint which acts as a server. Null if there is no server. private async Task Lookup(ImmutableList 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; } /// /// 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. /// private async Task IsServer(IEndpoint endpoint, CancellationToken cancellationToken = default) { return await Http.Get($"{endpoint.Address}/{Routes.EndpointsService}/{nameof(Server.IsServer)}", cancellationToken); } /// /// Thos method negotiates with other instances and tries to eventually chooses the one which will act /// as a server. /// /// The chosen endpoint private async Task Propose(ImmutableList 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($"{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)); } }