Implement timed cleanup.

This commit is contained in:
Nick Blakely 2020-08-08 17:26:41 -07:00
parent aa16e3507c
commit 75c2b9474e
14 changed files with 194 additions and 35 deletions

View File

@ -1,9 +1,5 @@
namespace Giants.DataContract
{
using System;
using System.Collections.Generic;
using System.Text;
public class PlayerInfo
{
public int Index { get; set; }

View File

@ -11,6 +11,9 @@
services.AddSingleton<IServerRegistryService, ServerRegistryService>();
services.AddSingleton<IServerRegistryStore, CosmosDbServerRegistryStore>();
services.AddSingleton<IDateTimeProvider, DefaultDateTimeProvider>();
services.AddHostedService<InitializerHostedService>();
services.AddHostedService<ServerRegistryCleanupService>();
}
}
}

View File

@ -9,6 +9,7 @@
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.12.0" />
<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" />
</ItemGroup>
<ItemGroup>

View File

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

View File

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

View File

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

View File

@ -46,7 +46,7 @@
public async Task<IEnumerable<ServerInfo>> 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);
}
}

View File

@ -32,10 +32,15 @@
public async Task<IEnumerable<TSelect>> GetItems<T, TSelect>(
Expression<Func<T, TSelect>> selectExpression,
string partitionKey,
Expression<Func<T, bool>> whereExpression = null)
Expression<Func<T, bool>> whereExpression = null,
string partitionKey = null)
where T : IIdentifiable
{
if (partitionKey == null)
{
partitionKey = typeof(T).Name;
}
IQueryable<T> query = this.container
.GetItemLinqQueryable<T>(requestOptions: new QueryRequestOptions
{
@ -131,5 +136,25 @@
this.container = containerResponse.Container;
}
public async Task DeleteItem<T>(
string id,
string partitionKey = null,
ItemRequestOptions requestOptions = null)
{
if (partitionKey == null)
{
partitionKey = typeof(T).Name;
}
try
{
await this.container.DeleteItemAsync<T>(id, new PartitionKey(partitionKey), requestOptions);
}
catch (CosmosException e) when (e.StatusCode == System.Net.HttpStatusCode.NotFound)
{
// Ignore
}
}
}
}

View File

@ -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<CosmosDbServerRegistryStore> logger;
private readonly IConfiguration configuration;
private CosmosDbClient client;
public CosmosDbServerRegistryStore(IConfiguration configuration)
public CosmosDbServerRegistryStore(
ILogger<CosmosDbServerRegistryStore> logger,
IConfiguration configuration)
{
this.logger = logger;
this.configuration = configuration;
}
public async Task<IEnumerable<ServerInfo>> GetServerInfos(
Expression<Func<ServerInfo, bool>> whereExpression = null)
Expression<Func<ServerInfo, bool>> whereExpression = null,
string partitionKey = null)
{
return await this.client.GetItems<ServerInfo>(whereExpression);
}
public async Task<IEnumerable<TSelect>> GetServerInfos<TSelect>(
Expression<Func<ServerInfo, TSelect>> selectExpression,
Expression<Func<ServerInfo, bool>> whereExpression = null,
string partitionKey = null)
{
return await this.client.GetItems<ServerInfo, TSelect>(
selectExpression: selectExpression,
whereExpression: whereExpression,
partitionKey: partitionKey);
}
public async Task<ServerInfo> GetServerInfo(string ipAddress)
{
return await this.client.GetItemById<ServerInfo>(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<string> ids, string partitionKey = null)
{
foreach (string id in ids)
{
this.logger.LogInformation("Deleting server for host IP {IPAddress}", id);
await this.client.DeleteItem<ServerInfo>(id, partitionKey);
}
}
public async Task Initialize()
@ -44,4 +73,4 @@
await this.client.Initialize();
}
}
}
}

View File

@ -7,12 +7,19 @@
public interface IServerRegistryStore
{
public Task Initialize();
Task Initialize();
public Task<IEnumerable<ServerInfo>> GetServerInfos(Expression<Func<ServerInfo, bool>> whereExpression = null);
Task<IEnumerable<ServerInfo>> GetServerInfos(Expression<Func<ServerInfo, bool>> whereExpression = null, string partitionKey = null);
public Task<ServerInfo> GetServerInfo(string ipAddress);
Task<IEnumerable<TSelect>> GetServerInfos<TSelect>(
Expression<Func<ServerInfo, TSelect>> selectExpression,
Expression<Func<ServerInfo, bool>> whereExpression = null,
string partitionKey = null);
public Task UpsertServerInfo(ServerInfo serverInfo);
Task<ServerInfo> GetServerInfo(string ipAddress);
Task UpsertServerInfo(ServerInfo serverInfo);
Task DeleteServers(IEnumerable<string> ids, string partitionKey = null);
}
}

View File

@ -4,7 +4,6 @@
using System.Collections.Generic;
using System.Text;
public interface IUpdaterStore
{
}

View File

@ -12,13 +12,6 @@
{
private ConcurrentDictionary<string, ServerInfo> servers = new ConcurrentDictionary<string, ServerInfo>();
public Task<IEnumerable<ServerInfo>> GetServerInfos(
Expression<Func<ServerInfo, bool>> whereExpression = null)
{
return Task.FromResult(
this.servers.Values.AsEnumerable());
}
public Task<ServerInfo> GetServerInfo(string ipAddress)
{
if (servers.ContainsKey(ipAddress))
@ -42,5 +35,25 @@
return Task.CompletedTask;
}
public Task<IEnumerable<ServerInfo>> GetServerInfos(Expression<Func<ServerInfo, bool>> whereExpression = null, string partitionKey = null)
{
throw new NotImplementedException();
}
public Task<IEnumerable<TSelect>> GetServerInfos<TSelect>(Expression<Func<ServerInfo, TSelect>> selectExpression, Expression<Func<ServerInfo, bool>> whereExpression = null, string partitionKey = null)
{
throw new NotImplementedException();
}
public Task DeleteServers(IEnumerable<string> ids, string partitionKey = null)
{
foreach (string id in ids)
{
this.servers.TryRemove(id, out _);
}
return Task.CompletedTask;
}
}
}

View File

@ -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<IActionContextAccessor, ActionContextAccessor>();
services.AddHostedService<InitializerHostedService>();
ServicesModule.RegisterServices(services, Configuration);

View File

@ -10,5 +10,6 @@
"DatabaseId": "DefaultDatabase",
"ContainerId": "DefaultContainer",
"ServerTimeoutPeriodInMinutes": "7",
"MaxServerCount": 1000
"ServerCleanupIntervalInMinutes": "1",
"MaxServerCount": 1000
}