mirror of
https://github.com/ncblakely/GiantsTools
synced 2024-11-22 22:25:37 +01:00
Support multiple servers per IP address.
This commit is contained in:
parent
25de26eb5c
commit
ff87767082
@ -7,6 +7,8 @@
|
||||
{
|
||||
Task DeleteServer(string ipAddress);
|
||||
|
||||
Task DeleteServer(string ipAddress, string gameName, int port);
|
||||
|
||||
Task<IEnumerable<ServerInfo>> GetAllServers();
|
||||
|
||||
Task AddServer(ServerInfo server);
|
||||
|
@ -4,16 +4,18 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AutoMapper;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
public class ServerRegistryService : IServerRegistryService
|
||||
{
|
||||
private static readonly string[] SupportedGameNames = new[] { "Giants" };
|
||||
private static readonly string[] SupportedGameNames = new[] { "Giants", "Giants Beta" };
|
||||
private readonly ILogger<ServerRegistryService> logger;
|
||||
private readonly IServerRegistryStore registryStore;
|
||||
private readonly IConfiguration configuration;
|
||||
private readonly int maxServerCount;
|
||||
private readonly int maxServersPerIpGame;
|
||||
|
||||
public ServerRegistryService(
|
||||
ILogger<ServerRegistryService> logger,
|
||||
@ -24,6 +26,7 @@
|
||||
this.registryStore = registryStore;
|
||||
this.configuration = configuration;
|
||||
this.maxServerCount = Convert.ToInt32(this.configuration["MaxServerCount"]);
|
||||
this.maxServersPerIpGame = Convert.ToInt32(this.configuration["MaxServersPerIpGame"]);
|
||||
}
|
||||
|
||||
public async Task AddServer(
|
||||
@ -37,6 +40,12 @@
|
||||
throw new ArgumentException($"Unsupported game name {serverInfo.GameName}", nameof(serverInfo));
|
||||
}
|
||||
|
||||
var existingServers = await this.registryStore.GetServerInfos(whereExpression: x => x.HostIpAddress == serverInfo.HostIpAddress);
|
||||
if (existingServers.GroupBy(g => g.GameName).Any(g => g.Count() > this.maxServersPerIpGame))
|
||||
{
|
||||
throw new InvalidOperationException("Exceeded maximum servers per IP.");
|
||||
}
|
||||
|
||||
await this.registryStore.UpsertServerInfo(serverInfo);
|
||||
}
|
||||
|
||||
@ -46,16 +55,36 @@
|
||||
.Take(this.maxServerCount);
|
||||
}
|
||||
|
||||
// Old API, soon to be deprecated
|
||||
public async Task DeleteServer(string ipAddress)
|
||||
{
|
||||
ArgumentUtility.CheckStringForNullOrEmpty(ipAddress, nameof(ipAddress));
|
||||
|
||||
ServerInfo serverInfo = await this.registryStore.GetServerInfo(ipAddress);
|
||||
var serverInfos = await this.registryStore.GetServerInfos(whereExpression: x => x.HostIpAddress == ipAddress);
|
||||
|
||||
if (serverInfo != null)
|
||||
foreach (var serverInfo in serverInfos)
|
||||
{
|
||||
await this.registryStore.DeleteServer(serverInfo.id);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteServer(string ipAddress, string gameName, int port)
|
||||
{
|
||||
ArgumentUtility.CheckStringForNullOrEmpty(ipAddress, nameof(ipAddress));
|
||||
ArgumentUtility.CheckStringForNullOrEmpty(gameName, nameof(gameName));
|
||||
ArgumentUtility.CheckForNonnegativeInt(port, nameof(port));
|
||||
|
||||
var existingServerInfo = (await this.registryStore.GetServerInfos(
|
||||
whereExpression:
|
||||
x => x.HostIpAddress == ipAddress &&
|
||||
x.Port == port &&
|
||||
x.GameName.Equals(gameName, StringComparison.OrdinalIgnoreCase)))
|
||||
.FirstOrDefault();
|
||||
|
||||
if (existingServerInfo != null)
|
||||
{
|
||||
await this.registryStore.DeleteServer(existingServerInfo.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -41,10 +41,11 @@
|
||||
bool includeExpired = false,
|
||||
string partitionKey = null)
|
||||
{
|
||||
ConcurrentDictionary<string, ServerInfo> serverInfo = await this.memoryCache.GetOrCreateAsync(CacheKeys.ServerInfo, this.PopulateCache);
|
||||
ConcurrentDictionary<string, IList<ServerInfo>> serverInfo = await this.memoryCache.GetOrCreateAsync(CacheKeys.ServerInfo, this.PopulateCache);
|
||||
|
||||
IQueryable<ServerInfo> serverInfoQuery = serverInfo
|
||||
.Values
|
||||
.SelectMany(s => s)
|
||||
.AsQueryable();
|
||||
|
||||
if (whereExpression != null)
|
||||
@ -67,10 +68,11 @@
|
||||
Expression<Func<ServerInfo, bool>> whereExpression = null,
|
||||
string partitionKey = null)
|
||||
{
|
||||
ConcurrentDictionary<string, ServerInfo> serverInfo = await this.memoryCache.GetOrCreateAsync(CacheKeys.ServerInfo, this.PopulateCache);
|
||||
ConcurrentDictionary<string, IList<ServerInfo>> serverInfo = await this.memoryCache.GetOrCreateAsync(CacheKeys.ServerInfo, this.PopulateCache);
|
||||
|
||||
IQueryable<ServerInfo> serverInfoQuery = serverInfo
|
||||
.Values
|
||||
.SelectMany(s => s)
|
||||
.AsQueryable();
|
||||
|
||||
if (serverInfoQuery != null)
|
||||
@ -88,42 +90,20 @@
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task<ServerInfo> GetServerInfo(string ipAddress)
|
||||
{
|
||||
ArgumentUtility.CheckStringForNullOrEmpty(ipAddress, nameof(ipAddress));
|
||||
|
||||
ConcurrentDictionary<string, ServerInfo> serverInfo = await this.memoryCache.GetOrCreateAsync(CacheKeys.ServerInfo, this.PopulateCache);
|
||||
if (serverInfo.ContainsKey(ipAddress))
|
||||
{
|
||||
try
|
||||
{
|
||||
return serverInfo[ipAddress];
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.logger.LogInformation("Cached server for {IPAddress} no longer found: {Exception}", ipAddress, e.ToString());
|
||||
// May have been removed from the cache by another thread. Ignore and query DB instead.
|
||||
}
|
||||
}
|
||||
|
||||
return await this.client.GetItemById<ServerInfo>(ipAddress);
|
||||
}
|
||||
|
||||
public async Task UpsertServerInfo(ServerInfo serverInfo)
|
||||
{
|
||||
ArgumentUtility.CheckForNull(serverInfo, nameof(serverInfo));
|
||||
|
||||
// Check cache before we write to store
|
||||
ConcurrentDictionary<string, ServerInfo> allServerInfo = await this.memoryCache.GetOrCreateAsync(CacheKeys.ServerInfo, this.PopulateCache);
|
||||
ConcurrentDictionary<string, IList<ServerInfo>> allServerInfo = await this.memoryCache.GetOrCreateAsync(CacheKeys.ServerInfo, this.PopulateCache);
|
||||
|
||||
if (allServerInfo.ContainsKey(serverInfo.HostIpAddress))
|
||||
{
|
||||
ServerInfo existingServerInfo = allServerInfo[serverInfo.HostIpAddress];
|
||||
ServerInfo existingServerInfo = FindExistingServerForHostIp(allServerInfo[serverInfo.HostIpAddress], serverInfo);
|
||||
|
||||
// DDOS protection: skip write to Cosmos if parameters have not changed,
|
||||
// or it's not been long enough.
|
||||
if (existingServerInfo.Equals(serverInfo)
|
||||
&& Math.Abs((existingServerInfo.LastHeartbeat - serverInfo.LastHeartbeat).TotalMinutes) < ServerRefreshIntervalInMinutes)
|
||||
if (existingServerInfo != null && Math.Abs((existingServerInfo.LastHeartbeat - serverInfo.LastHeartbeat).TotalMinutes) < ServerRefreshIntervalInMinutes)
|
||||
{
|
||||
this.logger.LogInformation("State for {IPAddress} is unchanged. Skipping write to store.", serverInfo.HostIpAddress);
|
||||
return;
|
||||
@ -142,11 +122,11 @@
|
||||
|
||||
if (allServerInfo.ContainsKey(serverInfo.HostIpAddress))
|
||||
{
|
||||
allServerInfo[serverInfo.HostIpAddress] = serverInfo;
|
||||
allServerInfo[serverInfo.HostIpAddress].Add(serverInfo);
|
||||
}
|
||||
else
|
||||
{
|
||||
allServerInfo.TryAdd(serverInfo.HostIpAddress, serverInfo);
|
||||
allServerInfo.TryAdd(serverInfo.HostIpAddress, new List<ServerInfo>() { serverInfo });
|
||||
}
|
||||
}
|
||||
|
||||
@ -155,8 +135,21 @@
|
||||
await this.client.DeleteItem<ServerInfo>(id, partitionKey);
|
||||
|
||||
// Remove from cache
|
||||
ConcurrentDictionary<string, ServerInfo> allServerInfo = await this.memoryCache.GetOrCreateAsync(CacheKeys.ServerInfo, this.PopulateCache);
|
||||
allServerInfo.TryRemove(id, out ServerInfo _);
|
||||
ConcurrentDictionary<string, IList<ServerInfo>> allServerInfo = await this.memoryCache.GetOrCreateAsync(CacheKeys.ServerInfo, this.PopulateCache);
|
||||
if (allServerInfo.ContainsKey(id))
|
||||
{
|
||||
var serverInfoCopy = allServerInfo[id].Where(s => s.id != id).ToList();
|
||||
if (!serverInfoCopy.Any())
|
||||
{
|
||||
// No remaining servers, remove the key
|
||||
allServerInfo.TryRemove(id, out IList<ServerInfo> _);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Shallow-copy and replace to keep thread safety guarantee
|
||||
allServerInfo[id] = serverInfoCopy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteServers(IEnumerable<string> ids, string partitionKey = null)
|
||||
@ -182,15 +175,39 @@
|
||||
await this.client.Initialize();
|
||||
}
|
||||
|
||||
private async Task<ConcurrentDictionary<string, ServerInfo>> PopulateCache(ICacheEntry entry)
|
||||
private async Task<ConcurrentDictionary<string, IList<ServerInfo>>> PopulateCache(ICacheEntry entry)
|
||||
{
|
||||
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(1);
|
||||
|
||||
IDictionary<string, ServerInfo> serverInfo =
|
||||
(await this.client.GetItems<ServerInfo>())
|
||||
.ToDictionary(x => x.HostIpAddress, y => y);
|
||||
var allServerInfo = (await this.client.GetItems<ServerInfo>());
|
||||
var serverInfoDictionary = new ConcurrentDictionary<string, IList<ServerInfo>>();
|
||||
|
||||
return new ConcurrentDictionary<string, ServerInfo>(serverInfo);
|
||||
foreach (var serverInfo in allServerInfo)
|
||||
{
|
||||
if (!serverInfoDictionary.ContainsKey(serverInfo.HostIpAddress))
|
||||
{
|
||||
serverInfoDictionary[serverInfo.HostIpAddress] = new List<ServerInfo>() { serverInfo };
|
||||
}
|
||||
else
|
||||
{
|
||||
serverInfoDictionary[serverInfo.HostIpAddress].Add(serverInfo);
|
||||
}
|
||||
}
|
||||
|
||||
return serverInfoDictionary;
|
||||
}
|
||||
|
||||
private static ServerInfo FindExistingServerForHostIp(IList<ServerInfo> serverInfoForHostIp, ServerInfo candidateServerInfo)
|
||||
{
|
||||
foreach (var existingServerInfo in serverInfoForHostIp)
|
||||
{
|
||||
if (existingServerInfo.Equals(candidateServerInfo))
|
||||
{
|
||||
return existingServerInfo;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@ -13,8 +13,6 @@
|
||||
|
||||
Task DeleteServers(IEnumerable<string> ids, string partitionKey = null);
|
||||
|
||||
Task<ServerInfo> GetServerInfo(string ipAddress);
|
||||
|
||||
Task<IEnumerable<ServerInfo>> GetServerInfos(Expression<Func<ServerInfo, bool>> whereExpression = null, bool includeExpired = false, string partitionKey = null);
|
||||
|
||||
Task<IEnumerable<TSelect>> GetServerInfos<TSelect>(
|
||||
|
@ -6,6 +6,7 @@
|
||||
|
||||
[ApiController]
|
||||
[ApiVersion("1.0")]
|
||||
[ApiVersion("1.1")]
|
||||
[Route("api/[controller]")]
|
||||
public class CommunityController : ControllerBase
|
||||
{
|
||||
|
@ -13,6 +13,7 @@
|
||||
|
||||
[ApiController]
|
||||
[ApiVersion("1.0")]
|
||||
[ApiVersion("1.1")]
|
||||
[Route("api/[controller]")]
|
||||
public class CrashReportsController : ControllerBase
|
||||
{
|
||||
|
@ -14,6 +14,7 @@ namespace Giants.Web.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[ApiVersion("1.0")]
|
||||
[ApiVersion("1.1")]
|
||||
[Route("api/[controller]")]
|
||||
public class ServersController : ControllerBase
|
||||
{
|
||||
@ -44,6 +45,17 @@ namespace Giants.Web.Controllers
|
||||
await this.serverRegistryService.DeleteServer(requestIpAddress);
|
||||
}
|
||||
|
||||
[HttpDelete]
|
||||
[MapToApiVersion("1.1")]
|
||||
public async Task DeleteServer(string gameName, int port)
|
||||
{
|
||||
string requestIpAddress = this.GetRequestIpAddress();
|
||||
|
||||
this.logger.LogInformation("Deleting server from {IPAddress}", requestIpAddress);
|
||||
|
||||
await this.serverRegistryService.DeleteServer(requestIpAddress, gameName, port);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IEnumerable<ServerInfoWithHostAddress>> GetServers()
|
||||
{
|
||||
|
@ -7,6 +7,7 @@ namespace Giants.WebApi.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[ApiVersion("1.0")]
|
||||
[ApiVersion("1.1")]
|
||||
[Route("api/[controller]")]
|
||||
public class VersionController : ControllerBase
|
||||
{
|
||||
|
@ -12,6 +12,7 @@
|
||||
"ServerTimeoutPeriodInMinutes": "7",
|
||||
"ServerCleanupIntervalInMinutes": "1",
|
||||
"MaxServerCount": 1000,
|
||||
"MaxServersPerIpGame": 5,
|
||||
"DiscordUri": "https://discord.gg/Avj4azU",
|
||||
"BlobConnectionString": "",
|
||||
"CrashBlobContainerName": "crashes"
|
||||
|
@ -19,6 +19,7 @@
|
||||
<RootNamespace>GiantsExp</RootNamespace>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<ProjectName>imp_gbs</ProjectName>
|
||||
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
|
||||
@ -74,7 +75,7 @@
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
|
||||
<CompileAs>Default</CompileAs>
|
||||
<LanguageStandard>stdcpplatest</LanguageStandard>
|
||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||
<ForcedIncludeFiles>stdafx.h</ForcedIncludeFiles>
|
||||
</ClCompile>
|
||||
<ResourceCompile>
|
||||
@ -110,7 +111,7 @@
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
|
||||
<CompileAs>Default</CompileAs>
|
||||
<LanguageStandard>stdcpplatest</LanguageStandard>
|
||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||
<ForcedIncludeFiles>stdafx.h</ForcedIncludeFiles>
|
||||
</ClCompile>
|
||||
<ResourceCompile>
|
||||
@ -142,7 +143,7 @@
|
||||
<PrecompiledHeader>Use</PrecompiledHeader>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
|
||||
<LanguageStandard>stdcpplatest</LanguageStandard>
|
||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||
<ForcedIncludeFiles>stdafx.h</ForcedIncludeFiles>
|
||||
</ClCompile>
|
||||
<ResourceCompile>
|
||||
|
Loading…
Reference in New Issue
Block a user