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/EndpointServer.cs

213 lines
7.2 KiB

using System.Collections.Immutable;
using Connected.Configuration;
using Connected.Net.Endpoints;
namespace Connected.Net.Server;
/// <summary>
/// This class handles communication between environment endpoints.
/// </summary>
internal sealed class EndpointServer : IEndpointServer
{
public event EventHandler<ServerChangedArgs>? Changed;
public event EventHandler Initialized;
private static ServerProposalArgs? _proposal;
public EndpointServer(IHttpService http, IConfigurationService configurationService)
{
Endpoints = ImmutableList<IEndpoint>.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<IEndpoint> 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<IEndpoint> 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);
}
/// <summary>
/// Creates <see cref="ServerProposalArgs"/>instance used when negotiating with
/// other instances for taking a server role.
/// </summary>
/// <returns></returns>
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
};
}
/// <summary>
/// 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.
/// </summary>
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);
}
/// <summary>
/// Returns <code>true</code> if this instance is currently the environment's server.
/// </summary>
public Task<bool> IsServer() => Task.FromResult(Server is null ? true : !Server.IsRemote);
/// <summary>
/// Returns the url of the currently active environment server.
/// </summary>
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;
}
}
/// <summary>
/// This method tries to resolve which process (<see cref="Endpoint"></see>) is the server in the
/// current environment (network).
/// </summary>
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();
}
/// <summary>
/// This method notifies all endpoints that we are acting as a server.
/// </summary>
/// <returns></returns>
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);
}
}
/// <summary>
/// Thos method is called from the Environment server notifying us it is
/// acting as a server.
/// </summary>
/// <param name="args"> The arguments acting as a proof that conditions
/// are met.</param>
/// <exception cref="NullReferenceException">If invalid endpoints id has been passed.</exception>
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
});
}
/// <summary>
/// Returns whether proposal from the remote instance is accepted by this instance.
/// </summary>
/// <param name="e">Remote instance proposal.</param>
/// <returns><code>true</code> if remote proposed arguments are chosen, <code>false</code> otherwise.</returns>
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;
}
}