Add cache layer to prevent request throttling under load.

This commit is contained in:
Nick Blakely 2020-08-09 02:10:33 -07:00
parent 81d5d86683
commit 208bcd1b25
11 changed files with 200 additions and 12 deletions

1
.gitignore vendored
View File

@ -10,6 +10,7 @@
*.userosscache
*.sln.docstates
appsettings.Development.json
profile.arm.json
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs

View File

@ -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);
}
}
}

View File

@ -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();
}
}
}

View File

@ -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();
}
}
}

View 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);
}
}
}

View 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);
}
}

View File

@ -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();
}
}
}

View File

@ -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>();

View File

@ -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>

View File

@ -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);
}
}
}

View File

@ -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;