Added caching to load balancer server list

This commit is contained in:
Derek S 2021-04-06 23:20:25 -05:00
parent 0a829c5646
commit fc75f3fa22
4 changed files with 106 additions and 58 deletions

View file

@ -18,6 +18,8 @@ namespace LightReflectiveMirror.LoadBalancing
[RestResource] [RestResource]
public class Endpoint public class Endpoint
{ {
public static string cachedServerList = "[]";
private LoadBalancerStats _stats private LoadBalancerStats _stats
{ {
get => new() get => new()
@ -39,7 +41,7 @@ namespace LightReflectiveMirror.LoadBalancing
public async Task ReceiveAuthKey(IHttpContext context) public async Task ReceiveAuthKey(IHttpContext context)
{ {
var req = context.Request; var req = context.Request;
string receivedAuthKey = req.Headers["x-Auth"]; string receivedAuthKey = req.Headers["Authorization"];
string endpointPort = req.Headers["x-EndpointPort"]; string endpointPort = req.Headers["x-EndpointPort"];
string gamePort = req.Headers["x-GamePort"]; string gamePort = req.Headers["x-GamePort"];
string publicIP = req.Headers["x-PIP"]; string publicIP = req.Headers["x-PIP"];
@ -69,6 +71,31 @@ namespace LightReflectiveMirror.LoadBalancing
await context.Response.SendResponseAsync(HttpStatusCode.Forbidden); await context.Response.SendResponseAsync(HttpStatusCode.Forbidden);
} }
/// <summary>
/// Called on the load balancer when a relay node had a change in their servers. This recompiles the cached values.
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
[RestRoute("Get", "/api/roomsupdated")]
public async Task ServerListUpdate(IHttpContext context)
{
// Dont allow unauthorizated access waste computing resources.
string auth = context.Request.Headers["Authorization"];
if(!string.IsNullOrEmpty(auth) && auth == Program.conf.AuthKey)
{
var relays = Program.instance.availableRelayServers.ToList();
List<Room> masterList = new();
for(int i = 0; i < relays.Count; i++)
{
masterList.AddRange(await Program.instance.RequestServerListFromNode(relays[i].Key.Address, relays[i].Key.EndpointPort));
}
cachedServerList = JsonConvert.SerializeObject(masterList);
}
}
/// <summary> /// <summary>
/// Hooks into from unity side, client will call this to /// Hooks into from unity side, client will call this to
/// find the least populated server to join /// find the least populated server to join
@ -102,13 +129,7 @@ namespace LightReflectiveMirror.LoadBalancing
// if the string is still dummy then theres no servers // if the string is still dummy then theres no servers
if (lowest.Key.Address != "Dummy") if (lowest.Key.Address != "Dummy")
{ {
// ping server to ensure its online. await context.Response.SendResponseAsync(JsonConvert.SerializeObject(lowest.Key));
var chosenServer = await Program.instance.ManualPingServer(lowest.Key.Address, lowest.Key.EndpointPort);
if (chosenServer.HasValue)
await context.Response.SendResponseAsync(JsonConvert.SerializeObject(lowest.Key));
else
await context.Response.SendResponseAsync(HttpStatusCode.BadGateway);
} }
else else
{ {
@ -124,24 +145,7 @@ namespace LightReflectiveMirror.LoadBalancing
[RestRoute("Get", "/api/masterlist/")] [RestRoute("Get", "/api/masterlist/")]
public async Task GetMasterServerList(IHttpContext context) public async Task GetMasterServerList(IHttpContext context)
{ {
var relays = Program.instance.availableRelayServers.ToList(); await context.Response.SendResponseAsync(cachedServerList);
List<Room> masterList = new();
foreach (var relay in relays)
{
var serversOnRelay = await Program.instance.GetServerListFromIndividualRelay(relay.Key.Address, relay.Key.EndpointPort);
if (serversOnRelay != null)
{
masterList.AddRange(serversOnRelay);
}
else { continue; }
}
// we have servers, send em!
if (masterList.Any())
await context.Response.SendResponseAsync(JsonConvert.SerializeObject(masterList));
// no servers or maybe no relays, fuck you
else
await context.Response.SendResponseAsync(HttpStatusCode.RangeNotSatisfiable);
} }
/// <summary> /// <summary>

View file

@ -12,8 +12,7 @@ namespace LightReflectiveMirror.LoadBalancing
partial class Program partial class Program
{ {
/// <summary> /// <summary>
/// Keeps track of all available relays. /// Keeps track of all the LRM nodes registered to the Load Balancer.
/// Key is server address, value is CCU.
/// </summary> /// </summary>
public Dictionary<RelayAddress, RelayServerInfo> availableRelayServers = new(); public Dictionary<RelayAddress, RelayServerInfo> availableRelayServers = new();
@ -64,6 +63,14 @@ namespace LightReflectiveMirror.LoadBalancing
} }
/// <summary>
/// Called when a new server requested that we add them to our load balancer.
/// </summary>
/// <param name="serverIP"></param>
/// <param name="port"></param>
/// <param name="endpointPort"></param>
/// <param name="publicIP"></param>
/// <returns></returns>
public async Task AddServer(string serverIP, ushort port, ushort endpointPort, string publicIP) public async Task AddServer(string serverIP, ushort port, ushort endpointPort, string publicIP)
{ {
var relayAddr = new RelayAddress { Port = port, EndpointPort = endpointPort, Address = publicIP, EndpointAddress = serverIP }; var relayAddr = new RelayAddress { Port = port, EndpointPort = endpointPort, Address = publicIP, EndpointAddress = serverIP };
@ -74,13 +81,22 @@ namespace LightReflectiveMirror.LoadBalancing
return; return;
} }
var stats = await ManualPingServer(serverIP, endpointPort); var stats = await RequestStatsFromNode(serverIP, endpointPort);
if (stats.HasValue) if (stats.HasValue)
{
Logger.ForceLogMessage($"LRM Node Registered! {serverIP}:{port}", ConsoleColor.Green);
availableRelayServers.Add(relayAddr, stats.Value); availableRelayServers.Add(relayAddr, stats.Value);
}
} }
public async Task<RelayServerInfo?> ManualPingServer(string serverIP, ushort port) /// <summary>
/// Called when we want to get the server info from a server.
/// </summary>
/// <param name="serverIP"></param>
/// <param name="port"></param>
/// <returns></returns>
public async Task<RelayServerInfo?> RequestStatsFromNode(string serverIP, ushort port)
{ {
using (WebClient wc = new WebClient()) using (WebClient wc = new WebClient())
{ {
@ -103,7 +119,38 @@ namespace LightReflectiveMirror.LoadBalancing
} }
} }
public async Task<List<Room>> GetServerListFromIndividualRelay(string serverIP, ushort port) /// <summary>
/// Called when we want to check if a server is alive.
/// </summary>
/// <param name="serverIP"></param>
/// <param name="port"></param>
/// <returns></returns>
public async Task<bool> HealthCheckNode(string serverIP, ushort port)
{
using (WebClient wc = new WebClient())
{
try
{
await wc.DownloadStringTaskAsync($"http://{serverIP}:{port}{API_PATH}");
// If it got to here, then the server is healthy!
return true;
}
catch (Exception e)
{
// Server failed to respond
return false;
}
}
}
/// <summary>
/// Called when we want to get the list of rooms in a specific LRM node.
/// </summary>
/// <param name="serverIP"></param>
/// <param name="port"></param>
/// <returns></returns>
public async Task<List<Room>> RequestServerListFromNode(string serverIP, ushort port)
{ {
using (WebClient wc = new WebClient()) using (WebClient wc = new WebClient())
{ {
@ -111,6 +158,8 @@ namespace LightReflectiveMirror.LoadBalancing
{ {
string receivedStats = await wc.DownloadStringTaskAsync($"http://{serverIP}:{port}/api/servers"); string receivedStats = await wc.DownloadStringTaskAsync($"http://{serverIP}:{port}/api/servers");
var stats = JsonConvert.DeserializeObject<List<Room>>(receivedStats); var stats = JsonConvert.DeserializeObject<List<Room>>(receivedStats);
// If they have no servers, it will return null as json for some reason.
if (stats == null) if (stats == null)
return new List<Room>(); return new List<Room>();
else else
@ -124,6 +173,9 @@ namespace LightReflectiveMirror.LoadBalancing
} }
} }
/// <summary>
/// A thread constantly running and making sure LRM nodes are still healthy.
/// </summary>
async void PingServers() async void PingServers()
{ {
while (true) while (true)
@ -135,32 +187,9 @@ namespace LightReflectiveMirror.LoadBalancing
for (int i = 0; i < keys.Count; i++) for (int i = 0; i < keys.Count; i++)
{ {
string url = $"http://{keys[i].EndpointAddress}:{keys[i].EndpointPort}{API_PATH}"; if(!await HealthCheckNode(keys[i].EndpointAddress, keys[i].EndpointPort))
using (WebClient wc = new WebClient())
{ {
try Logger.ForceLogMessage($"Server {keys[i].Address}:{keys[i].Port} failed a health check, removing from load balancer.", ConsoleColor.Red);
{
var serverStats = wc.DownloadString(url);
var deserializedData = JsonConvert.DeserializeObject<RelayServerInfo>(serverStats);
Logger.WriteLogMessage("Server " + keys[i].Address + " still exists, keeping in collection.");
// get current server list
deserializedData.serversConnectedToRelay = await GetServerListFromIndividualRelay(keys[i].Address, keys[i].Port);
if (availableRelayServers.ContainsKey(keys[i]))
availableRelayServers[keys[i]] = deserializedData;
else
availableRelayServers.Add(keys[i], deserializedData);
}
catch
{
// server doesnt exist anymore probably
Logger.WriteLogMessage("Server " + keys[i] + " does not exist anymore, removing", ConsoleColor.Red);
availableRelayServers.Remove(keys[i]);
}
} }
} }

View file

@ -41,6 +41,9 @@ namespace LightReflectiveMirror.Endpoints
{ {
_cachedServerList = JsonConvert.SerializeObject(_rooms, Formatting.Indented); _cachedServerList = JsonConvert.SerializeObject(_rooms, Formatting.Indented);
_cachedCompressedServerList = _cachedServerList.Compress(); _cachedCompressedServerList = _cachedServerList.Compress();
if (Program.conf.UseLoadBalancer)
Program.instance.UpdateLoadbalancerServers();
} }
[RestRoute("Get", "/api/stats")] [RestRoute("Get", "/api/stats")]

View file

@ -201,6 +201,19 @@ namespace LightReflectiveMirror
} }
} }
public async void UpdateLoadbalancerServers()
{
try
{
using (WebClient wc = new())
{
wc.Headers.Add("Authorization", conf.LoadBalancerAuthKey);
await wc.DownloadStringTaskAsync($"http://{conf.LoadBalancerAddress}:{conf.LoadBalancerPort}/api/roomsupdated");
}
}
catch {} // LLB might be down, ignore.
}
private async Task<bool> RegisterSelfToLoadBalancer() private async Task<bool> RegisterSelfToLoadBalancer()
{ {
Endpoint.lastPing = DateTime.Now; Endpoint.lastPing = DateTime.Now;
@ -215,7 +228,7 @@ namespace LightReflectiveMirror
string gamePort = 7777.ToString(); string gamePort = 7777.ToString();
HttpWebRequest authReq = (HttpWebRequest)WebRequest.Create(uri); HttpWebRequest authReq = (HttpWebRequest)WebRequest.Create(uri);
authReq.Headers.Add("x-Auth", conf.LoadBalancerAuthKey); authReq.Headers.Add("Authorization", conf.LoadBalancerAuthKey);
authReq.Headers.Add("x-EndpointPort", endpointPort); authReq.Headers.Add("x-EndpointPort", endpointPort);
authReq.Headers.Add("x-GamePort", gamePort); authReq.Headers.Add("x-GamePort", gamePort);
authReq.Headers.Add("x-PIP", publicIP); // Public IP authReq.Headers.Add("x-PIP", publicIP); // Public IP
@ -230,7 +243,6 @@ namespace LightReflectiveMirror
WriteLogMessage("Error registering - Load Balancer probably timed out.", ConsoleColor.Red); WriteLogMessage("Error registering - Load Balancer probably timed out.", ConsoleColor.Red);
return false; return false;
} }
} }
void CheckMethods(Type type) void CheckMethods(Type type)