using System.Collections.Immutable; using Connected.Configuration; using Connected.Net.Endpoints; namespace Connected.Net.Server; /// /// This class handles communication between environment endpoints. /// internal sealed class EndpointServer : IEndpointServer { public event EventHandler? Changed; public event EventHandler Initialized; private static ServerProposalArgs? _proposal; public EndpointServer(IHttpService http, IConfigurationService configurationService) { Endpoints = ImmutableList.Empty; ConfigurationService = configurationService; Http = http; if (ConfigurationService is null) throw new NullReferenceException(nameof(IConfigurationService)); if (Http is null) throw new NullReferenceException(nameof(IHttpService)); } private ImmutableList Endpoints { get; set; } private IHttpService Http { get; } private IConfigurationService ConfigurationService { get; } private EndpointServerDescriptor? Server { get; set; } private bool IsInitialized { get; set; } internal static ServerProposalArgs? ProposalArgs => _proposal; public async Task Initialize(ImmutableList endpoints, CancellationToken cancellationToken) { if (IsInitialized) throw new SysException(this, SR.ErrInitialized); Endpoints = endpoints; ValidateInstance(); InitializeProposal(); await ResolveServer(cancellationToken); IsInitialized = true; Initialized?.Invoke(this, EventArgs.Empty); } /// /// Creates instance used when negotiating with /// other instances for taking a server role. /// /// private void InitializeProposal() { // If there are no endpoints defined we probably won't need arguments anyway. if (Endpoints is null || !Endpoints.Any()) { _proposal = new ServerProposalArgs(); return; } // Create proposal arguments with the id of this endpoint. _proposal = new ServerProposalArgs { Id = Endpoints.First(f => string.Equals(f.Address, ConfigurationService.Endpoint.Address, StringComparison.OrdinalIgnoreCase)).Id }; } /// /// Validatates this instance against the endpoints configuration. /// If endpoints configuration does not have any records we are /// probably (but not necessarily true) in a single instance environment. /// Note that endpoints table should always contain at least one record. /// If endpoints contain at least one record that means our address must /// match with one record in the endpoints configuration. /// private void ValidateInstance() { if (Endpoints is null || !Endpoints.Any()) return; if (Endpoints.FirstOrDefault(f => string.Equals(f.Address, ConfigurationService.Endpoint.Address, StringComparison.OrdinalIgnoreCase)) is null) throw new SysException(this, SR.ValInstanceNotRegistered); } /// /// Returns true if this instance is currently the environment's server. /// public Task IsServer() => Task.FromResult(Server is null ? true : !Server.IsRemote); /// /// Returns the url of the currently active environment server. /// public string ServerUrl { get { if (Server is null) throw new NullReferenceException(nameof(Server)); if (Server.Endpoint is null) throw new NullReferenceException(nameof(Server.Endpoint)); return Server.Endpoint.Address; } } /// /// This method tries to resolve which process () is the server in the /// current environment (network). /// private async Task ResolveServer(CancellationToken cancellationToken) { var protocol = new ServerProtocol(Endpoints, this, ConfigurationService, Http); var newServer = await protocol.ResolveServer(cancellationToken); // Server didn't change. if (string.Equals(newServer?.Address, Server?.Endpoint?.Address, StringComparison.OrdinalIgnoreCase)) return; // We have a new server. If we are the server we must notify other endpoints that we are acting as // an endpoint server. If not, the chosen server will notify us. Server = new EndpointServerDescriptor(await protocol.ResolveServer(cancellationToken), ConfigurationService); if (!await IsServer()) return; await AnnounceServerChange(); } /// /// This method notifies all endpoints that we are acting as a server. /// /// private async Task AnnounceServerChange() { if (Endpoints is null || !Endpoints.Any()) return; foreach (var endpoint in Endpoints) { // No need to notify ourselves. if (string.Equals(endpoint.Address, ConfigurationService.Endpoint.Address, StringComparison.OrdinalIgnoreCase)) continue; await Http.Post($"{Routes.EndpointsService}/{nameof(NotifyServerChange)}", ProposalArgs); } } /// /// Thos method is called from the Environment server notifying us it is /// acting as a server. /// /// The arguments acting as a proof that conditions /// are met. /// If invalid endpoints id has been passed. public async Task NotifyServerChange(ServerProposalArgs args) { // We should probaby valdate the arguments here again in case // some race condition happened and more thar one server chose // to be the one if (Endpoints.FirstOrDefault(f => f.Id == args.Id) is not IEndpoint endpoint) throw new NullReferenceException($"{SR.ErrEndpointNull} ({args.Id})"); if (Server is not null && Server.Endpoint is not null) { // Already points to the active server if (Server.Endpoint.Id == args.Id) return; } Server = new EndpointServerDescriptor(endpoint, ConfigurationService); await Task.CompletedTask; } private async Task ChangeServer(IEndpoint endpoint) { // Nothing changed if (endpoint is null && (Server is null || Server.Endpoint is null)) return; if (endpoint is not null && Server is not null && Server.Endpoint is not null && endpoint.Id == Server.Endpoint.Id) return; // Now change the server's endpoint Server = new EndpointServerDescriptor(endpoint, ConfigurationService); // Notify this environment about server change. Changed?.Invoke(this, new ServerChangedArgs { Endpoint = endpoint, IsRemote = Server.IsRemote }); } /// /// Returns whether proposal from the remote instance is accepted by this instance. /// /// Remote instance proposal. /// true if remote proposed arguments are chosen, false otherwise. public bool Propose(ServerProposalArgs e) { // The algorithm is simple, the oldest arguments win. That means the instance than started first // is the winner. if (e.TimeStamp < ProposalArgs.TimeStamp) return true; else if (e.TimeStamp > ProposalArgs.TimeStamp) return false; // There is a small chance both timestamps are the same. In that case we choose the winner based // on weight. if (e.Weight < ProposalArgs.Weight) return true; else if (e.Weight > ProposalArgs.Weight) return false; // Well, this is really almost impossible but if it happened the system wouldn't be able to decide // which one to choose so we will play the random game. return Random.Shared.Next(1, 100) > 50; } }