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.
213 lines
7.2 KiB
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;
|
|
}
|
|
}
|