mirror of
https://github.com/ncblakely/GiantsTools
synced 2024-11-24 15:15:37 +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
|
||||
*.sln.docstates
|
||||
appsettings.Development.json
|
||||
profile.arm.json
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
@ -1,4 +1,6 @@
|
||||
namespace Giants.DataContract
|
||||
using System;
|
||||
|
||||
namespace Giants.DataContract
|
||||
{
|
||||
public class PlayerInfo
|
||||
{
|
||||
@ -11,5 +13,20 @@
|
||||
public int Deaths { 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.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text;
|
||||
using System.Linq;
|
||||
|
||||
public class ServerInfo
|
||||
{
|
||||
@ -13,12 +12,11 @@
|
||||
public string GameName { get; set; }
|
||||
|
||||
[Required]
|
||||
[StringLength(100)]
|
||||
public string Version { get; set; }
|
||||
public VersionInfo Version { get; set; }
|
||||
|
||||
[Required]
|
||||
[StringLength(100)]
|
||||
public string SessionName { get; set; } // was "HostName"
|
||||
public string SessionName { get; set; }
|
||||
|
||||
[Required]
|
||||
public int Port { get; set; }
|
||||
@ -52,5 +50,42 @@
|
||||
|
||||
[Required]
|
||||
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
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text;
|
||||
|
||||
public class ServerInfoWithHostAddress : ServerInfo
|
||||
{
|
||||
[Required]
|
||||
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 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
|
||||
{
|
||||
using Giants.Services.Core;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
@ -11,6 +12,7 @@
|
||||
services.AddSingleton<IServerRegistryService, ServerRegistryService>();
|
||||
services.AddSingleton<IServerRegistryStore, CosmosDbServerRegistryStore>();
|
||||
services.AddSingleton<IDateTimeProvider, DefaultDateTimeProvider>();
|
||||
services.AddSingleton<IMemoryCache, MemoryCache>();
|
||||
|
||||
services.AddHostedService<InitializerService>();
|
||||
services.AddHostedService<ServerRegistryCleanupService>();
|
||||
|
@ -7,9 +7,12 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoMapper" Version="10.0.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.DependencyInjection.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>
|
||||
|
@ -1,28 +1,38 @@
|
||||
namespace Giants.Services
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Giants.Services.Core;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
public class ServerRegistryService : IServerRegistryService
|
||||
{
|
||||
private static readonly string[] SupportedGameNames = new[] { "Giants" };
|
||||
private readonly ILogger<ServerRegistryService> logger;
|
||||
private readonly IServerRegistryStore registryStore;
|
||||
private readonly IConfiguration configuration;
|
||||
private readonly IDateTimeProvider dateTimeProvider;
|
||||
private readonly IMemoryCache memoryCache;
|
||||
private readonly TimeSpan timeoutPeriod;
|
||||
private readonly int maxServerCount;
|
||||
|
||||
public ServerRegistryService(
|
||||
ILogger<ServerRegistryService> logger,
|
||||
IServerRegistryStore registryStore,
|
||||
IConfiguration configuration,
|
||||
IDateTimeProvider dateTimeProvider)
|
||||
IDateTimeProvider dateTimeProvider,
|
||||
IMemoryCache memoryCache)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.registryStore = registryStore;
|
||||
this.configuration = configuration;
|
||||
this.dateTimeProvider = dateTimeProvider;
|
||||
this.memoryCache = memoryCache;
|
||||
this.timeoutPeriod = TimeSpan.FromMinutes(Convert.ToDouble(this.configuration["ServerTimeoutPeriodInMinutes"]));
|
||||
this.maxServerCount = Convert.ToInt32(this.configuration["MaxServerCount"]);
|
||||
}
|
||||
@ -33,14 +43,58 @@
|
||||
ArgumentUtility.CheckForNull(serverInfo, nameof(serverInfo));
|
||||
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);
|
||||
|
||||
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()
|
||||
{
|
||||
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))
|
||||
.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
|
||||
{
|
||||
[ApiController]
|
||||
[Route("[controller]")]
|
||||
[Route("api/[controller]")]
|
||||
public class ServersController : ControllerBase
|
||||
{
|
||||
private readonly IMapper mapper;
|
||||
|
Loading…
Reference in New Issue
Block a user