diff --git a/Giants.DataContract/Contracts/PlayerInfo.cs b/Giants.DataContract/Contracts/PlayerInfo.cs index b5f44a5..33bae03 100644 --- a/Giants.DataContract/Contracts/PlayerInfo.cs +++ b/Giants.DataContract/Contracts/PlayerInfo.cs @@ -1,9 +1,5 @@ namespace Giants.DataContract { - using System; - using System.Collections.Generic; - using System.Text; - public class PlayerInfo { public int Index { get; set; } diff --git a/Giants.Services/Core/ServicesModule.cs b/Giants.Services/Core/ServicesModule.cs index e2f9ef5..a39344a 100644 --- a/Giants.Services/Core/ServicesModule.cs +++ b/Giants.Services/Core/ServicesModule.cs @@ -11,6 +11,9 @@ services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + + services.AddHostedService(); + services.AddHostedService(); } } } diff --git a/Giants.Services/Giants.Services.csproj b/Giants.Services/Giants.Services.csproj index 2d99ef3..8797d56 100644 --- a/Giants.Services/Giants.Services.csproj +++ b/Giants.Services/Giants.Services.csproj @@ -9,6 +9,7 @@ + diff --git a/Giants.Services/Services/IServerRegistryCleanupService.cs b/Giants.Services/Services/IServerRegistryCleanupService.cs new file mode 100644 index 0000000..93b76ce --- /dev/null +++ b/Giants.Services/Services/IServerRegistryCleanupService.cs @@ -0,0 +1,13 @@ +namespace Giants.Services +{ + using System; + using System.Collections.Generic; + using System.Text; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Extensions.Hosting; + + public interface IServerRegistryCleanupService : IHostedService + { + } +} diff --git a/Giants.WebApi/InitializerHostedService.cs b/Giants.Services/Services/InitializerHostedService.cs similarity index 73% rename from Giants.WebApi/InitializerHostedService.cs rename to Giants.Services/Services/InitializerHostedService.cs index b6ff7d0..3480484 100644 --- a/Giants.WebApi/InitializerHostedService.cs +++ b/Giants.Services/Services/InitializerHostedService.cs @@ -1,13 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Giants.Services; -using Microsoft.Extensions.Hosting; - -namespace Giants.WebApi +namespace Giants.Services { + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Extensions.Hosting; + public class InitializerHostedService : IHostedService { private readonly IServerRegistryStore serverRegistryStore; diff --git a/Giants.Services/Services/ServerRegistryCleanupService.cs b/Giants.Services/Services/ServerRegistryCleanupService.cs new file mode 100644 index 0000000..997d7c7 --- /dev/null +++ b/Giants.Services/Services/ServerRegistryCleanupService.cs @@ -0,0 +1,77 @@ +namespace Giants.Services +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading; + using System.Threading.Tasks; + using Giants.Services.Core; + using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.Logging; + + public class ServerRegistryCleanupService : IServerRegistryCleanupService, IDisposable + { + private readonly ILogger logger; + private readonly IConfiguration configuration; + private readonly IServerRegistryStore serverRegistryStore; + private readonly IDateTimeProvider dateTimeProvider; + private readonly TimeSpan timeoutPeriod; + private readonly TimeSpan cleanupInterval; + private Timer timer; + + public ServerRegistryCleanupService( + ILogger logger, + IConfiguration configuration, + IServerRegistryStore serverRegistryStore, + IDateTimeProvider dateTimeProvider) + { + this.logger = logger; + this.configuration = configuration; + this.serverRegistryStore = serverRegistryStore; + this.dateTimeProvider = dateTimeProvider; + this.timeoutPeriod = TimeSpan.FromMinutes(Convert.ToDouble(this.configuration["ServerTimeoutPeriodInMinutes"])); + this.cleanupInterval = TimeSpan.FromMinutes(Convert.ToDouble(this.configuration["ServerCleanupIntervalInMinutes"])); + } + + public Task StartAsync(CancellationToken cancellationToken) + { + this.timer = new Timer(TimerCallback, null, TimeSpan.Zero, this.cleanupInterval); + + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + this.timer?.Change(Timeout.Infinite, 0); + + return Task.CompletedTask; + } + + public void Dispose() + { + this.timer?.Dispose(); + } + + private void TimerCallback(object state) + { + this.CleanupServers().GetAwaiter().GetResult(); + } + + private async Task CleanupServers() + { + List expiredServers = (await this + .serverRegistryStore + .GetServerInfos(whereExpression: s => s.LastHeartbeat < (this.dateTimeProvider.UtcNow - this.timeoutPeriod))) + .Select(s => s.id) + .ToList(); + + if (expiredServers.Any()) + { + logger.LogInformation("Cleaning up {Count} servers.", expiredServers.Count); + + await this.serverRegistryStore.DeleteServers(expiredServers); + } + } + } +} diff --git a/Giants.Services/Services/ServerRegistryService.cs b/Giants.Services/Services/ServerRegistryService.cs index 1510721..a740077 100644 --- a/Giants.Services/Services/ServerRegistryService.cs +++ b/Giants.Services/Services/ServerRegistryService.cs @@ -46,7 +46,7 @@ public async Task> GetAllServers() { return (await this.registryStore - .GetServerInfos(c => c.LastHeartbeat > this.dateTimeProvider.UtcNow - this.timeoutPeriod)) + .GetServerInfos(whereExpression: c => c.LastHeartbeat > this.dateTimeProvider.UtcNow - this.timeoutPeriod)) .Take(this.maxServerCount); } } diff --git a/Giants.Services/Store/CosmosDbClient.cs b/Giants.Services/Store/CosmosDbClient.cs index 8c312ab..94d26a6 100644 --- a/Giants.Services/Store/CosmosDbClient.cs +++ b/Giants.Services/Store/CosmosDbClient.cs @@ -32,10 +32,15 @@ public async Task> GetItems( Expression> selectExpression, - string partitionKey, - Expression> whereExpression = null) + Expression> whereExpression = null, + string partitionKey = null) where T : IIdentifiable { + if (partitionKey == null) + { + partitionKey = typeof(T).Name; + } + IQueryable query = this.container .GetItemLinqQueryable(requestOptions: new QueryRequestOptions { @@ -131,5 +136,25 @@ this.container = containerResponse.Container; } + + public async Task DeleteItem( + string id, + string partitionKey = null, + ItemRequestOptions requestOptions = null) + { + if (partitionKey == null) + { + partitionKey = typeof(T).Name; + } + + try + { + await this.container.DeleteItemAsync(id, new PartitionKey(partitionKey), requestOptions); + } + catch (CosmosException e) when (e.StatusCode == System.Net.HttpStatusCode.NotFound) + { + // Ignore + } + } } } diff --git a/Giants.Services/Store/CosmosDbServerRegistryStore.cs b/Giants.Services/Store/CosmosDbServerRegistryStore.cs index 9521f8c..c89575f 100644 --- a/Giants.Services/Store/CosmosDbServerRegistryStore.cs +++ b/Giants.Services/Store/CosmosDbServerRegistryStore.cs @@ -6,23 +6,40 @@ using System.Threading.Tasks; using Microsoft.Azure.Cosmos; using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.Logging; public class CosmosDbServerRegistryStore : IServerRegistryStore { + private readonly ILogger logger; private readonly IConfiguration configuration; private CosmosDbClient client; - public CosmosDbServerRegistryStore(IConfiguration configuration) + public CosmosDbServerRegistryStore( + ILogger logger, + IConfiguration configuration) { + this.logger = logger; this.configuration = configuration; } public async Task> GetServerInfos( - Expression> whereExpression = null) + Expression> whereExpression = null, + string partitionKey = null) { return await this.client.GetItems(whereExpression); } + public async Task> GetServerInfos( + Expression> selectExpression, + Expression> whereExpression = null, + string partitionKey = null) + { + return await this.client.GetItems( + selectExpression: selectExpression, + whereExpression: whereExpression, + partitionKey: partitionKey); + } + public async Task GetServerInfo(string ipAddress) { return await this.client.GetItemById(ipAddress); @@ -30,7 +47,19 @@ public async Task UpsertServerInfo(ServerInfo serverInfo) { - await this.client.UpsertItem(serverInfo, new PartitionKey(serverInfo.DocumentType)); + await this.client.UpsertItem( + item: serverInfo, + partitionKey: new PartitionKey(serverInfo.DocumentType)); + } + + public async Task DeleteServers(IEnumerable ids, string partitionKey = null) + { + foreach (string id in ids) + { + this.logger.LogInformation("Deleting server for host IP {IPAddress}", id); + + await this.client.DeleteItem(id, partitionKey); + } } public async Task Initialize() @@ -44,4 +73,4 @@ await this.client.Initialize(); } } -} +} \ No newline at end of file diff --git a/Giants.Services/Store/IServerRegistryStore.cs b/Giants.Services/Store/IServerRegistryStore.cs index b54224e..d0f5016 100644 --- a/Giants.Services/Store/IServerRegistryStore.cs +++ b/Giants.Services/Store/IServerRegistryStore.cs @@ -7,12 +7,19 @@ public interface IServerRegistryStore { - public Task Initialize(); + Task Initialize(); - public Task> GetServerInfos(Expression> whereExpression = null); + Task> GetServerInfos(Expression> whereExpression = null, string partitionKey = null); - public Task GetServerInfo(string ipAddress); + Task> GetServerInfos( + Expression> selectExpression, + Expression> whereExpression = null, + string partitionKey = null); - public Task UpsertServerInfo(ServerInfo serverInfo); + Task GetServerInfo(string ipAddress); + + Task UpsertServerInfo(ServerInfo serverInfo); + + Task DeleteServers(IEnumerable ids, string partitionKey = null); } } diff --git a/Giants.Services/Store/IUpdaterStore.cs b/Giants.Services/Store/IUpdaterStore.cs index df0058e..8446d24 100644 --- a/Giants.Services/Store/IUpdaterStore.cs +++ b/Giants.Services/Store/IUpdaterStore.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Text; - public interface IUpdaterStore { } diff --git a/Giants.Services/Store/InMemoryServerRegistryStore.cs b/Giants.Services/Store/InMemoryServerRegistryStore.cs index 4c3683d..c1ba7df 100644 --- a/Giants.Services/Store/InMemoryServerRegistryStore.cs +++ b/Giants.Services/Store/InMemoryServerRegistryStore.cs @@ -12,13 +12,6 @@ { private ConcurrentDictionary servers = new ConcurrentDictionary(); - public Task> GetServerInfos( - Expression> whereExpression = null) - { - return Task.FromResult( - this.servers.Values.AsEnumerable()); - } - public Task GetServerInfo(string ipAddress) { if (servers.ContainsKey(ipAddress)) @@ -42,5 +35,25 @@ return Task.CompletedTask; } + + public Task> GetServerInfos(Expression> whereExpression = null, string partitionKey = null) + { + throw new NotImplementedException(); + } + + public Task> GetServerInfos(Expression> selectExpression, Expression> whereExpression = null, string partitionKey = null) + { + throw new NotImplementedException(); + } + + public Task DeleteServers(IEnumerable ids, string partitionKey = null) + { + foreach (string id in ids) + { + this.servers.TryRemove(id, out _); + } + + return Task.CompletedTask; + } } } diff --git a/Giants.WebApi/Startup.cs b/Giants.WebApi/Startup.cs index 1be2929..281e689 100644 --- a/Giants.WebApi/Startup.cs +++ b/Giants.WebApi/Startup.cs @@ -1,6 +1,5 @@ using AutoMapper; using Giants.Services; -using Giants.WebApi; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Infrastructure; @@ -27,7 +26,7 @@ namespace Giants.Web services.AddHttpContextAccessor(); services.TryAddSingleton(); - services.AddHostedService(); + ServicesModule.RegisterServices(services, Configuration); diff --git a/Giants.WebApi/appsettings.json b/Giants.WebApi/appsettings.json index ca3573c..53040ff 100644 --- a/Giants.WebApi/appsettings.json +++ b/Giants.WebApi/appsettings.json @@ -10,5 +10,6 @@ "DatabaseId": "DefaultDatabase", "ContainerId": "DefaultContainer", "ServerTimeoutPeriodInMinutes": "7", - "MaxServerCount": 1000 + "ServerCleanupIntervalInMinutes": "1", + "MaxServerCount": 1000 }