diff --git a/Giants.Services/Services/ServerRegistryService.cs b/Giants.Services/Services/ServerRegistryService.cs index 90f7d55..ddf2323 100644 --- a/Giants.Services/Services/ServerRegistryService.cs +++ b/Giants.Services/Services/ServerRegistryService.cs @@ -16,24 +16,16 @@ private readonly ILogger 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 logger, IServerRegistryStore registryStore, - IConfiguration configuration, - IDateTimeProvider dateTimeProvider, - IMemoryCache memoryCache) + IConfiguration configuration) { 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"]); } @@ -48,41 +40,13 @@ throw new ArgumentException($"Unsupported game name {serverInfo.GameName}", nameof(serverInfo)); } - // Check cache before we write to store - ConcurrentDictionary allServerInfo = await this.memoryCache.GetOrCreateAsync(CacheKeys.ServerInfo, this.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> GetAllServers() { - ConcurrentDictionary serverInfo = await this.memoryCache.GetOrCreateAsync(CacheKeys.ServerInfo, this.PopulateCache); - - return serverInfo.Values; + return (await this.registryStore.GetServerInfos()) + .Take(this.maxServerCount); } public async Task DeleteServer(string ipAddress) @@ -96,17 +60,5 @@ await this.registryStore.DeleteServer(serverInfo.id); } } - - private async Task> PopulateCache(ICacheEntry entry) - { - entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(1); - - IDictionary serverInfo = (await this.registryStore - .GetServerInfos(whereExpression: c => c.LastHeartbeat > this.dateTimeProvider.UtcNow - this.timeoutPeriod)) - .Take(this.maxServerCount) - .ToDictionary(x => x.HostIpAddress, y => y); - - return new ConcurrentDictionary(serverInfo); - } } } diff --git a/Giants.Services/Store/CosmosDbServerRegistryStore.cs b/Giants.Services/Store/CosmosDbServerRegistryStore.cs index fac462f..adee78f 100644 --- a/Giants.Services/Store/CosmosDbServerRegistryStore.cs +++ b/Giants.Services/Store/CosmosDbServerRegistryStore.cs @@ -1,10 +1,14 @@ namespace Giants.Services { using System; + using System.Collections.Concurrent; using System.Collections.Generic; + using System.Linq; using System.Linq.Expressions; using System.Threading.Tasks; + using Giants.Services.Core; using Microsoft.Azure.Cosmos; + using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; @@ -12,21 +16,41 @@ { private readonly ILogger logger; private readonly IConfiguration configuration; + private readonly IMemoryCache memoryCache; + private readonly IDateTimeProvider dateTimeProvider; + private readonly TimeSpan timeoutPeriod; private CosmosDbClient client; public CosmosDbServerRegistryStore( ILogger logger, - IConfiguration configuration) + IConfiguration configuration, + IMemoryCache memoryCache, + IDateTimeProvider dateTimeProvider) { this.logger = logger; this.configuration = configuration; + this.memoryCache = memoryCache; + this.dateTimeProvider = dateTimeProvider; + this.timeoutPeriod = TimeSpan.FromMinutes(Convert.ToDouble(this.configuration["ServerTimeoutPeriodInMinutes"])); } public async Task> GetServerInfos( Expression> whereExpression = null, string partitionKey = null) { - return await this.client.GetItems(whereExpression); + ConcurrentDictionary serverInfo = await this.memoryCache.GetOrCreateAsync(CacheKeys.ServerInfo, this.PopulateCache); + + IQueryable serverInfoQuery = serverInfo + .Values + .AsQueryable(); + + if (whereExpression != null) + { + serverInfoQuery = serverInfoQuery.Where(whereExpression); + } + + return serverInfoQuery.Where(c => c.LastHeartbeat > this.dateTimeProvider.UtcNow - this.timeoutPeriod) + .ToList(); } public async Task> GetServerInfos( @@ -34,18 +58,40 @@ Expression> whereExpression = null, string partitionKey = null) { - ArgumentUtility.CheckForNull(selectExpression, nameof(selectExpression)); + ConcurrentDictionary serverInfo = await this.memoryCache.GetOrCreateAsync(CacheKeys.ServerInfo, this.PopulateCache); - return await this.client.GetItems( - selectExpression: selectExpression, - whereExpression: whereExpression, - partitionKey: partitionKey); + IQueryable serverInfoQuery = serverInfo + .Values + .AsQueryable(); + + if (serverInfoQuery != null) + { + serverInfoQuery = serverInfoQuery.Where(whereExpression); + } + + return serverInfoQuery.Where(c => c.LastHeartbeat > this.dateTimeProvider.UtcNow - this.timeoutPeriod) + .Select(selectExpression) + .ToList(); } public async Task GetServerInfo(string ipAddress) { ArgumentUtility.CheckStringForNullOrEmpty(ipAddress, nameof(ipAddress)); + ConcurrentDictionary 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(ipAddress); } @@ -53,14 +99,45 @@ { ArgumentUtility.CheckForNull(serverInfo, nameof(serverInfo)); + // Check cache before we write to store + ConcurrentDictionary allServerInfo = await this.memoryCache.GetOrCreateAsync(CacheKeys.ServerInfo, this.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.client.UpsertItem( item: serverInfo, partitionKey: new PartitionKey(serverInfo.DocumentType)); + + 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 DeleteServer(string id, string partitionKey = null) { await this.client.DeleteItem(id, partitionKey); + + // Remove from cache + ConcurrentDictionary allServerInfo = await this.memoryCache.GetOrCreateAsync(CacheKeys.ServerInfo, this.PopulateCache); + allServerInfo.TryRemove(id, out ServerInfo _); } public async Task DeleteServers(IEnumerable ids, string partitionKey = null) @@ -71,7 +148,7 @@ { this.logger.LogInformation("Deleting server for host IP {IPAddress}", id); - await this.client.DeleteItem(id, partitionKey); + await this.DeleteServer(id, partitionKey); } } @@ -85,5 +162,16 @@ await this.client.Initialize(); } + + private async Task> PopulateCache(ICacheEntry entry) + { + entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(1); + + IDictionary serverInfo = + (await this.client.GetItems()) + .ToDictionary(x => x.HostIpAddress, y => y); + + return new ConcurrentDictionary(serverInfo); + } } } \ No newline at end of file diff --git a/Giants.WebApi/Properties/launchSettings.json b/Giants.WebApi/Properties/launchSettings.json index 29bcacc..aa888bb 100644 --- a/Giants.WebApi/Properties/launchSettings.json +++ b/Giants.WebApi/Properties/launchSettings.json @@ -1,5 +1,4 @@ -{ - "$schema": "http://json.schemastore.org/launchsettings.json", +{ "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, @@ -8,11 +7,11 @@ "sslPort": 44304 } }, + "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, - "launchUrl": "weatherforecast", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } @@ -21,10 +20,10 @@ "commandName": "Project", "launchBrowser": true, "launchUrl": "weatherforecast", - "applicationUrl": "https://localhost:5001;http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" - } + }, + "applicationUrl": "https://localhost:5001;http://localhost:5000" } } -} +} \ No newline at end of file