mirror of
https://github.com/ncblakely/GiantsTools
synced 2024-11-21 21:55:38 +01:00
Add cache layer to prevent request throttling under load.
This commit is contained in:
parent
81d5d86683
commit
208bcd1b25
1
.gitignore
vendored
1
.gitignore
vendored
@ -10,6 +10,7 @@
|
|||||||
*.userosscache
|
*.userosscache
|
||||||
*.sln.docstates
|
*.sln.docstates
|
||||||
appsettings.Development.json
|
appsettings.Development.json
|
||||||
|
profile.arm.json
|
||||||
|
|
||||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||||
*.userprefs
|
*.userprefs
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
namespace Giants.DataContract
|
using System;
|
||||||
|
|
||||||
|
namespace Giants.DataContract
|
||||||
{
|
{
|
||||||
public class PlayerInfo
|
public class PlayerInfo
|
||||||
{
|
{
|
||||||
@ -11,5 +13,20 @@
|
|||||||
public int Deaths { get; set; }
|
public int Deaths { get; set; }
|
||||||
|
|
||||||
public string TeamName { get; set; }
|
public string TeamName { get; set; }
|
||||||
|
|
||||||
|
public override bool Equals(object obj)
|
||||||
|
{
|
||||||
|
return obj is PlayerInfo info &&
|
||||||
|
Index == info.Index &&
|
||||||
|
Name == info.Name &&
|
||||||
|
Frags == info.Frags &&
|
||||||
|
Deaths == info.Deaths &&
|
||||||
|
TeamName == info.TeamName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
return HashCode.Combine(Index, Name, Frags, Deaths, TeamName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Linq;
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
public class ServerInfo
|
public class ServerInfo
|
||||||
{
|
{
|
||||||
@ -13,12 +12,11 @@
|
|||||||
public string GameName { get; set; }
|
public string GameName { get; set; }
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
[StringLength(100)]
|
public VersionInfo Version { get; set; }
|
||||||
public string Version { get; set; }
|
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
[StringLength(100)]
|
[StringLength(100)]
|
||||||
public string SessionName { get; set; } // was "HostName"
|
public string SessionName { get; set; }
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
public int Port { get; set; }
|
public int Port { get; set; }
|
||||||
@ -52,5 +50,42 @@
|
|||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
public IList<PlayerInfo> PlayerInfo { get; set; }
|
public IList<PlayerInfo> PlayerInfo { get; set; }
|
||||||
|
|
||||||
|
public override bool Equals(object obj)
|
||||||
|
{
|
||||||
|
return obj is ServerInfo info &&
|
||||||
|
GameName == info.GameName &&
|
||||||
|
EqualityComparer<VersionInfo>.Default.Equals(Version, info.Version) &&
|
||||||
|
SessionName == info.SessionName &&
|
||||||
|
Port == info.Port &&
|
||||||
|
MapName == info.MapName &&
|
||||||
|
GameType == info.GameType &&
|
||||||
|
NumPlayers == info.NumPlayers &&
|
||||||
|
GameState == info.GameState &&
|
||||||
|
TimeLimit == info.TimeLimit &&
|
||||||
|
FragLimit == info.FragLimit &&
|
||||||
|
TeamFragLimit == info.TeamFragLimit &&
|
||||||
|
FirstBaseComplete == info.FirstBaseComplete &&
|
||||||
|
PlayerInfo.SequenceEqual(info.PlayerInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
HashCode hash = new HashCode();
|
||||||
|
hash.Add(GameName);
|
||||||
|
hash.Add(Version);
|
||||||
|
hash.Add(SessionName);
|
||||||
|
hash.Add(Port);
|
||||||
|
hash.Add(MapName);
|
||||||
|
hash.Add(GameType);
|
||||||
|
hash.Add(NumPlayers);
|
||||||
|
hash.Add(GameState);
|
||||||
|
hash.Add(TimeLimit);
|
||||||
|
hash.Add(FragLimit);
|
||||||
|
hash.Add(TeamFragLimit);
|
||||||
|
hash.Add(FirstBaseComplete);
|
||||||
|
hash.Add(PlayerInfo);
|
||||||
|
return hash.ToHashCode();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,26 @@
|
|||||||
namespace Giants.DataContract
|
namespace Giants.DataContract
|
||||||
{
|
{
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
public class ServerInfoWithHostAddress : ServerInfo
|
public class ServerInfoWithHostAddress : ServerInfo
|
||||||
{
|
{
|
||||||
[Required]
|
[Required]
|
||||||
public string HostIpAddress { get; set; }
|
public string HostIpAddress { get; set; }
|
||||||
|
|
||||||
|
public override bool Equals(object obj)
|
||||||
|
{
|
||||||
|
return obj is ServerInfoWithHostAddress address &&
|
||||||
|
base.Equals(obj) &&
|
||||||
|
HostIpAddress == address.HostIpAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
HashCode hash = new HashCode();
|
||||||
|
hash.Add(base.GetHashCode());
|
||||||
|
hash.Add(HostIpAddress);
|
||||||
|
return hash.ToHashCode();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
34
Giants.DataContract/Contracts/VersionInfo.cs
Normal file
34
Giants.DataContract/Contracts/VersionInfo.cs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
namespace Giants.DataContract
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
public class VersionInfo
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public int Build { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public int Major { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public int Minor { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public int Revision { get; set; }
|
||||||
|
|
||||||
|
public override bool Equals(object obj)
|
||||||
|
{
|
||||||
|
return obj is VersionInfo info &&
|
||||||
|
Build == info.Build &&
|
||||||
|
Major == info.Major &&
|
||||||
|
Minor == info.Minor &&
|
||||||
|
Revision == info.Revision;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
return HashCode.Combine(Build, Major, Minor, Revision);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
Giants.Services/Core/CacheKeys.cs
Normal file
11
Giants.Services/Core/CacheKeys.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
namespace Giants.Services
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
public static class CacheKeys
|
||||||
|
{
|
||||||
|
public const string ServerInfo = nameof(ServerInfo);
|
||||||
|
}
|
||||||
|
}
|
@ -12,5 +12,23 @@
|
|||||||
public DateTime LastHeartbeat { get; set; }
|
public DateTime LastHeartbeat { get; set; }
|
||||||
|
|
||||||
public string DocumentType => nameof(ServerInfo);
|
public string DocumentType => nameof(ServerInfo);
|
||||||
|
|
||||||
|
public override bool Equals(object obj)
|
||||||
|
{
|
||||||
|
return obj is ServerInfo info &&
|
||||||
|
base.Equals(obj) &&
|
||||||
|
HostIpAddress == info.HostIpAddress &&
|
||||||
|
DocumentType == info.DocumentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
HashCode hash = new HashCode();
|
||||||
|
hash.Add(base.GetHashCode());
|
||||||
|
hash.Add(id);
|
||||||
|
hash.Add(HostIpAddress);
|
||||||
|
hash.Add(DocumentType);
|
||||||
|
return hash.ToHashCode();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
namespace Giants.Services
|
namespace Giants.Services
|
||||||
{
|
{
|
||||||
using Giants.Services.Core;
|
using Giants.Services.Core;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
@ -11,6 +12,7 @@
|
|||||||
services.AddSingleton<IServerRegistryService, ServerRegistryService>();
|
services.AddSingleton<IServerRegistryService, ServerRegistryService>();
|
||||||
services.AddSingleton<IServerRegistryStore, CosmosDbServerRegistryStore>();
|
services.AddSingleton<IServerRegistryStore, CosmosDbServerRegistryStore>();
|
||||||
services.AddSingleton<IDateTimeProvider, DefaultDateTimeProvider>();
|
services.AddSingleton<IDateTimeProvider, DefaultDateTimeProvider>();
|
||||||
|
services.AddSingleton<IMemoryCache, MemoryCache>();
|
||||||
|
|
||||||
services.AddHostedService<InitializerService>();
|
services.AddHostedService<InitializerService>();
|
||||||
services.AddHostedService<ServerRegistryCleanupService>();
|
services.AddHostedService<ServerRegistryCleanupService>();
|
||||||
|
@ -7,9 +7,12 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AutoMapper" Version="10.0.0" />
|
<PackageReference Include="AutoMapper" Version="10.0.0" />
|
||||||
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.12.0" />
|
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.12.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.6" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.6" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="3.1.6" />
|
<PackageReference Include="Microsoft.Extensions.Configuration" Version="3.1.6" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.6" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.6" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.6" />
|
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.6" />
|
||||||
|
<PackageReference Include="System.Runtime.Caching" Version="4.7.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -1,28 +1,38 @@
|
|||||||
namespace Giants.Services
|
namespace Giants.Services
|
||||||
{
|
{
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Giants.Services.Core;
|
using Giants.Services.Core;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
public class ServerRegistryService : IServerRegistryService
|
public class ServerRegistryService : IServerRegistryService
|
||||||
{
|
{
|
||||||
|
private static readonly string[] SupportedGameNames = new[] { "Giants" };
|
||||||
|
private readonly ILogger<ServerRegistryService> logger;
|
||||||
private readonly IServerRegistryStore registryStore;
|
private readonly IServerRegistryStore registryStore;
|
||||||
private readonly IConfiguration configuration;
|
private readonly IConfiguration configuration;
|
||||||
private readonly IDateTimeProvider dateTimeProvider;
|
private readonly IDateTimeProvider dateTimeProvider;
|
||||||
|
private readonly IMemoryCache memoryCache;
|
||||||
private readonly TimeSpan timeoutPeriod;
|
private readonly TimeSpan timeoutPeriod;
|
||||||
private readonly int maxServerCount;
|
private readonly int maxServerCount;
|
||||||
|
|
||||||
public ServerRegistryService(
|
public ServerRegistryService(
|
||||||
|
ILogger<ServerRegistryService> logger,
|
||||||
IServerRegistryStore registryStore,
|
IServerRegistryStore registryStore,
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
IDateTimeProvider dateTimeProvider)
|
IDateTimeProvider dateTimeProvider,
|
||||||
|
IMemoryCache memoryCache)
|
||||||
{
|
{
|
||||||
|
this.logger = logger;
|
||||||
this.registryStore = registryStore;
|
this.registryStore = registryStore;
|
||||||
this.configuration = configuration;
|
this.configuration = configuration;
|
||||||
this.dateTimeProvider = dateTimeProvider;
|
this.dateTimeProvider = dateTimeProvider;
|
||||||
|
this.memoryCache = memoryCache;
|
||||||
this.timeoutPeriod = TimeSpan.FromMinutes(Convert.ToDouble(this.configuration["ServerTimeoutPeriodInMinutes"]));
|
this.timeoutPeriod = TimeSpan.FromMinutes(Convert.ToDouble(this.configuration["ServerTimeoutPeriodInMinutes"]));
|
||||||
this.maxServerCount = Convert.ToInt32(this.configuration["MaxServerCount"]);
|
this.maxServerCount = Convert.ToInt32(this.configuration["MaxServerCount"]);
|
||||||
}
|
}
|
||||||
@ -33,14 +43,58 @@
|
|||||||
ArgumentUtility.CheckForNull(serverInfo, nameof(serverInfo));
|
ArgumentUtility.CheckForNull(serverInfo, nameof(serverInfo));
|
||||||
ArgumentUtility.CheckStringForNullOrEmpty(serverInfo.HostIpAddress, nameof(serverInfo.HostIpAddress));
|
ArgumentUtility.CheckStringForNullOrEmpty(serverInfo.HostIpAddress, nameof(serverInfo.HostIpAddress));
|
||||||
|
|
||||||
|
if (!SupportedGameNames.Contains(serverInfo.GameName, StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"Unsupported game name {serverInfo.GameName}", nameof(serverInfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check cache before we write to store
|
||||||
|
ConcurrentDictionary<string, ServerInfo> allServerInfo = await this.memoryCache.GetOrCreateAsync(CacheKeys.ServerInfo, PopulateCache);
|
||||||
|
|
||||||
|
if (allServerInfo.ContainsKey(serverInfo.HostIpAddress))
|
||||||
|
{
|
||||||
|
if (allServerInfo[serverInfo.HostIpAddress].Equals(serverInfo))
|
||||||
|
{
|
||||||
|
this.logger.LogInformation("State for {IPAddress} is unchanged. Skipping write to store.", serverInfo.HostIpAddress);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.logger.LogInformation("State for {IPAddress} has changed. Committing update to store.", serverInfo.HostIpAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
await this.registryStore.UpsertServerInfo(serverInfo);
|
await this.registryStore.UpsertServerInfo(serverInfo);
|
||||||
|
|
||||||
|
this.logger.LogInformation("Updating cache for request from {IPAddress}.", serverInfo.HostIpAddress);
|
||||||
|
|
||||||
|
if (allServerInfo.ContainsKey(serverInfo.HostIpAddress))
|
||||||
|
{
|
||||||
|
allServerInfo[serverInfo.HostIpAddress] = serverInfo;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
allServerInfo.TryAdd(serverInfo.HostIpAddress, serverInfo);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<ServerInfo>> GetAllServers()
|
public async Task<IEnumerable<ServerInfo>> GetAllServers()
|
||||||
{
|
{
|
||||||
return (await this.registryStore
|
ConcurrentDictionary<string, ServerInfo> serverInfo = await this.memoryCache.GetOrCreateAsync(CacheKeys.ServerInfo, PopulateCache);
|
||||||
|
|
||||||
|
return serverInfo.Values;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<ConcurrentDictionary<string, ServerInfo>> PopulateCache(ICacheEntry entry)
|
||||||
|
{
|
||||||
|
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(1);
|
||||||
|
|
||||||
|
IDictionary<string, ServerInfo> serverInfo = (await this.registryStore
|
||||||
.GetServerInfos(whereExpression: c => c.LastHeartbeat > this.dateTimeProvider.UtcNow - this.timeoutPeriod))
|
.GetServerInfos(whereExpression: c => c.LastHeartbeat > this.dateTimeProvider.UtcNow - this.timeoutPeriod))
|
||||||
.Take(this.maxServerCount);
|
.Take(this.maxServerCount)
|
||||||
|
.ToDictionary(x => x.HostIpAddress, y => y);
|
||||||
|
|
||||||
|
return new ConcurrentDictionary<string, ServerInfo>(serverInfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ using Microsoft.Extensions.Logging;
|
|||||||
namespace Giants.Web.Controllers
|
namespace Giants.Web.Controllers
|
||||||
{
|
{
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("[controller]")]
|
[Route("api/[controller]")]
|
||||||
public class ServersController : ControllerBase
|
public class ServersController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly IMapper mapper;
|
private readonly IMapper mapper;
|
||||||
|
Loading…
Reference in New Issue
Block a user