1
0
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:
Nick Blakely 2020-11-06 15:30:31 -08:00
parent 25de26eb5c
commit ff87767082
10 changed files with 107 additions and 44 deletions

View File

@ -7,6 +7,8 @@
{ {
Task DeleteServer(string ipAddress); Task DeleteServer(string ipAddress);
Task DeleteServer(string ipAddress, string gameName, int port);
Task<IEnumerable<ServerInfo>> GetAllServers(); Task<IEnumerable<ServerInfo>> GetAllServers();
Task AddServer(ServerInfo server); Task AddServer(ServerInfo server);

View File

@ -4,16 +4,18 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using AutoMapper;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
public class ServerRegistryService : IServerRegistryService 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 ILogger<ServerRegistryService> logger;
private readonly IServerRegistryStore registryStore; private readonly IServerRegistryStore registryStore;
private readonly IConfiguration configuration; private readonly IConfiguration configuration;
private readonly int maxServerCount; private readonly int maxServerCount;
private readonly int maxServersPerIpGame;
public ServerRegistryService( public ServerRegistryService(
ILogger<ServerRegistryService> logger, ILogger<ServerRegistryService> logger,
@ -24,6 +26,7 @@
this.registryStore = registryStore; this.registryStore = registryStore;
this.configuration = configuration; this.configuration = configuration;
this.maxServerCount = Convert.ToInt32(this.configuration["MaxServerCount"]); this.maxServerCount = Convert.ToInt32(this.configuration["MaxServerCount"]);
this.maxServersPerIpGame = Convert.ToInt32(this.configuration["MaxServersPerIpGame"]);
} }
public async Task AddServer( public async Task AddServer(
@ -37,6 +40,12 @@
throw new ArgumentException($"Unsupported game name {serverInfo.GameName}", nameof(serverInfo)); 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); await this.registryStore.UpsertServerInfo(serverInfo);
} }
@ -46,16 +55,36 @@
.Take(this.maxServerCount); .Take(this.maxServerCount);
} }
// Old API, soon to be deprecated
public async Task DeleteServer(string ipAddress) public async Task DeleteServer(string ipAddress)
{ {
ArgumentUtility.CheckStringForNullOrEmpty(ipAddress, nameof(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); 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);
}
}
} }
} }

View File

@ -41,10 +41,11 @@
bool includeExpired = false, bool includeExpired = false,
string partitionKey = 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 IQueryable<ServerInfo> serverInfoQuery = serverInfo
.Values .Values
.SelectMany(s => s)
.AsQueryable(); .AsQueryable();
if (whereExpression != null) if (whereExpression != null)
@ -67,10 +68,11 @@
Expression<Func<ServerInfo, bool>> whereExpression = null, Expression<Func<ServerInfo, bool>> whereExpression = null,
string partitionKey = 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 IQueryable<ServerInfo> serverInfoQuery = serverInfo
.Values .Values
.SelectMany(s => s)
.AsQueryable(); .AsQueryable();
if (serverInfoQuery != null) if (serverInfoQuery != null)
@ -88,42 +90,20 @@
.ToList(); .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) public async Task UpsertServerInfo(ServerInfo serverInfo)
{ {
ArgumentUtility.CheckForNull(serverInfo, nameof(serverInfo)); ArgumentUtility.CheckForNull(serverInfo, nameof(serverInfo));
// Check cache before we write to store // 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)) 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, // DDOS protection: skip write to Cosmos if parameters have not changed,
// or it's not been long enough. // or it's not been long enough.
if (existingServerInfo.Equals(serverInfo) if (existingServerInfo != null && Math.Abs((existingServerInfo.LastHeartbeat - serverInfo.LastHeartbeat).TotalMinutes) < ServerRefreshIntervalInMinutes)
&& Math.Abs((existingServerInfo.LastHeartbeat - serverInfo.LastHeartbeat).TotalMinutes) < ServerRefreshIntervalInMinutes)
{ {
this.logger.LogInformation("State for {IPAddress} is unchanged. Skipping write to store.", serverInfo.HostIpAddress); this.logger.LogInformation("State for {IPAddress} is unchanged. Skipping write to store.", serverInfo.HostIpAddress);
return; return;
@ -142,11 +122,11 @@
if (allServerInfo.ContainsKey(serverInfo.HostIpAddress)) if (allServerInfo.ContainsKey(serverInfo.HostIpAddress))
{ {
allServerInfo[serverInfo.HostIpAddress] = serverInfo; allServerInfo[serverInfo.HostIpAddress].Add(serverInfo);
} }
else 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); await this.client.DeleteItem<ServerInfo>(id, partitionKey);
// Remove from cache // Remove from cache
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);
allServerInfo.TryRemove(id, out ServerInfo _); 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) public async Task DeleteServers(IEnumerable<string> ids, string partitionKey = null)
@ -182,15 +175,39 @@
await this.client.Initialize(); 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); entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(1);
IDictionary<string, ServerInfo> serverInfo = var allServerInfo = (await this.client.GetItems<ServerInfo>());
(await this.client.GetItems<ServerInfo>()) var serverInfoDictionary = new ConcurrentDictionary<string, IList<ServerInfo>>();
.ToDictionary(x => x.HostIpAddress, y => y);
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;
} }
} }
} }

View File

@ -13,8 +13,6 @@
Task DeleteServers(IEnumerable<string> ids, string partitionKey = null); 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<ServerInfo>> GetServerInfos(Expression<Func<ServerInfo, bool>> whereExpression = null, bool includeExpired = false, string partitionKey = null);
Task<IEnumerable<TSelect>> GetServerInfos<TSelect>( Task<IEnumerable<TSelect>> GetServerInfos<TSelect>(

View File

@ -6,6 +6,7 @@
[ApiController] [ApiController]
[ApiVersion("1.0")] [ApiVersion("1.0")]
[ApiVersion("1.1")]
[Route("api/[controller]")] [Route("api/[controller]")]
public class CommunityController : ControllerBase public class CommunityController : ControllerBase
{ {

View File

@ -13,6 +13,7 @@
[ApiController] [ApiController]
[ApiVersion("1.0")] [ApiVersion("1.0")]
[ApiVersion("1.1")]
[Route("api/[controller]")] [Route("api/[controller]")]
public class CrashReportsController : ControllerBase public class CrashReportsController : ControllerBase
{ {

View File

@ -14,6 +14,7 @@ namespace Giants.Web.Controllers
{ {
[ApiController] [ApiController]
[ApiVersion("1.0")] [ApiVersion("1.0")]
[ApiVersion("1.1")]
[Route("api/[controller]")] [Route("api/[controller]")]
public class ServersController : ControllerBase public class ServersController : ControllerBase
{ {
@ -44,6 +45,17 @@ namespace Giants.Web.Controllers
await this.serverRegistryService.DeleteServer(requestIpAddress); 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] [HttpGet]
public async Task<IEnumerable<ServerInfoWithHostAddress>> GetServers() public async Task<IEnumerable<ServerInfoWithHostAddress>> GetServers()
{ {

View File

@ -7,6 +7,7 @@ namespace Giants.WebApi.Controllers
{ {
[ApiController] [ApiController]
[ApiVersion("1.0")] [ApiVersion("1.0")]
[ApiVersion("1.1")]
[Route("api/[controller]")] [Route("api/[controller]")]
public class VersionController : ControllerBase public class VersionController : ControllerBase
{ {

View File

@ -12,6 +12,7 @@
"ServerTimeoutPeriodInMinutes": "7", "ServerTimeoutPeriodInMinutes": "7",
"ServerCleanupIntervalInMinutes": "1", "ServerCleanupIntervalInMinutes": "1",
"MaxServerCount": 1000, "MaxServerCount": 1000,
"MaxServersPerIpGame": 5,
"DiscordUri": "https://discord.gg/Avj4azU", "DiscordUri": "https://discord.gg/Avj4azU",
"BlobConnectionString": "", "BlobConnectionString": "",
"CrashBlobContainerName": "crashes" "CrashBlobContainerName": "crashes"

View File

@ -19,6 +19,7 @@
<RootNamespace>GiantsExp</RootNamespace> <RootNamespace>GiantsExp</RootNamespace>
<Keyword>Win32Proj</Keyword> <Keyword>Win32Proj</Keyword>
<ProjectName>imp_gbs</ProjectName> <ProjectName>imp_gbs</ProjectName>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup> </PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" /> <Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
@ -74,7 +75,7 @@
<WarningLevel>Level3</WarningLevel> <WarningLevel>Level3</WarningLevel>
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat> <DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
<CompileAs>Default</CompileAs> <CompileAs>Default</CompileAs>
<LanguageStandard>stdcpplatest</LanguageStandard> <LanguageStandard>stdcpp17</LanguageStandard>
<ForcedIncludeFiles>stdafx.h</ForcedIncludeFiles> <ForcedIncludeFiles>stdafx.h</ForcedIncludeFiles>
</ClCompile> </ClCompile>
<ResourceCompile> <ResourceCompile>
@ -110,7 +111,7 @@
<WarningLevel>Level3</WarningLevel> <WarningLevel>Level3</WarningLevel>
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat> <DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
<CompileAs>Default</CompileAs> <CompileAs>Default</CompileAs>
<LanguageStandard>stdcpplatest</LanguageStandard> <LanguageStandard>stdcpp17</LanguageStandard>
<ForcedIncludeFiles>stdafx.h</ForcedIncludeFiles> <ForcedIncludeFiles>stdafx.h</ForcedIncludeFiles>
</ClCompile> </ClCompile>
<ResourceCompile> <ResourceCompile>
@ -142,7 +143,7 @@
<PrecompiledHeader>Use</PrecompiledHeader> <PrecompiledHeader>Use</PrecompiledHeader>
<WarningLevel>Level3</WarningLevel> <WarningLevel>Level3</WarningLevel>
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat> <DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
<LanguageStandard>stdcpplatest</LanguageStandard> <LanguageStandard>stdcpp17</LanguageStandard>
<ForcedIncludeFiles>stdafx.h</ForcedIncludeFiles> <ForcedIncludeFiles>stdafx.h</ForcedIncludeFiles>
</ClCompile> </ClCompile>
<ResourceCompile> <ResourceCompile>