diff --git a/.gitignore b/.gitignore index 4ce6fdd..a5a4f29 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ *.user *.userosscache *.sln.docstates +appsettings.Development.json # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs diff --git a/Giants.Services/Core/DefaultDateTimeProvider.cs b/Giants.Services/Core/DefaultDateTimeProvider.cs new file mode 100644 index 0000000..3c09031 --- /dev/null +++ b/Giants.Services/Core/DefaultDateTimeProvider.cs @@ -0,0 +1,11 @@ +namespace Giants.Services.Core +{ + using System; + + public class DefaultDateTimeProvider : IDateTimeProvider + { + public static DefaultDateTimeProvider Instance { get; } = new DefaultDateTimeProvider(); + + public DateTime UtcNow => DateTime.UtcNow; + } +} diff --git a/Giants.Services/Core/Entities/IIdentifiable.cs b/Giants.Services/Core/Entities/IIdentifiable.cs new file mode 100644 index 0000000..13268d2 --- /dev/null +++ b/Giants.Services/Core/Entities/IIdentifiable.cs @@ -0,0 +1,9 @@ +namespace Giants.Services.Core.Entities +{ + public interface IIdentifiable + { + string id { get; } + + string DocumentType { get; } + } +} diff --git a/Giants.Services/Core/Entities/ServerInfo.cs b/Giants.Services/Core/Entities/ServerInfo.cs index 0e63ce7..6e00351 100644 --- a/Giants.Services/Core/Entities/ServerInfo.cs +++ b/Giants.Services/Core/Entities/ServerInfo.cs @@ -1,11 +1,16 @@ namespace Giants.Services { using System; + using Giants.Services.Core.Entities; - public class ServerInfo : DataContract.ServerInfo + public class ServerInfo : DataContract.ServerInfo, IIdentifiable { + public string id => HostIpAddress; + public string HostIpAddress { get; set; } public DateTime LastHeartbeat { get; set; } + + public string DocumentType => nameof(ServerInfo); } } diff --git a/Giants.Services/Core/IDateTimeProvider.cs b/Giants.Services/Core/IDateTimeProvider.cs new file mode 100644 index 0000000..74b981c --- /dev/null +++ b/Giants.Services/Core/IDateTimeProvider.cs @@ -0,0 +1,9 @@ +namespace Giants.Services.Core +{ + using System; + + public interface IDateTimeProvider + { + DateTime UtcNow { get; } + } +} diff --git a/Giants.Services/Core/ServicesModule.cs b/Giants.Services/Core/ServicesModule.cs index 0f9ced8..e2f9ef5 100644 --- a/Giants.Services/Core/ServicesModule.cs +++ b/Giants.Services/Core/ServicesModule.cs @@ -1,13 +1,16 @@ namespace Giants.Services { + using Giants.Services.Core; + using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; public static class ServicesModule { - public static void RegisterServices(IServiceCollection services) + public static void RegisterServices(IServiceCollection services, IConfiguration configuration) { services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); } } } diff --git a/Giants.Services/Giants.Services.csproj b/Giants.Services/Giants.Services.csproj index 921189f..2d99ef3 100644 --- a/Giants.Services/Giants.Services.csproj +++ b/Giants.Services/Giants.Services.csproj @@ -6,6 +6,8 @@ + + diff --git a/Giants.Services/Services/IServerRegistryService.cs b/Giants.Services/Services/IServerRegistryService.cs index d81a8f9..991dea7 100644 --- a/Giants.Services/Services/IServerRegistryService.cs +++ b/Giants.Services/Services/IServerRegistryService.cs @@ -10,6 +10,6 @@ { Task> GetAllServers(); - Task AddServer(IPAddress ipAddress, ServerInfo server); + Task AddServer(ServerInfo server); } } diff --git a/Giants.Services/Services/IServerRegistryStore.cs b/Giants.Services/Services/IServerRegistryStore.cs deleted file mode 100644 index ba3fd53..0000000 --- a/Giants.Services/Services/IServerRegistryStore.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Giants.Services -{ - using System; - using System.Collections.Generic; - using System.Net; - using System.Text; - using System.Threading.Tasks; - - public interface IServerRegistryStore - { - public Task> GetAllServerInfos(); - - public Task GetServerInfo(IPAddress ipAddress); - - public Task UpsertServerInfo(IPAddress ipAddress, ServerInfo serverInfo); - } -} diff --git a/Giants.Services/Services/ServerRegistryService.cs b/Giants.Services/Services/ServerRegistryService.cs index a2e370e..1510721 100644 --- a/Giants.Services/Services/ServerRegistryService.cs +++ b/Giants.Services/Services/ServerRegistryService.cs @@ -2,34 +2,52 @@ { using System; using System.Collections.Generic; - using System.Net; + using System.Linq; using System.Threading.Tasks; + using Giants.Services.Core; + using Microsoft.Extensions.Configuration; public class ServerRegistryService : IServerRegistryService { private readonly IServerRegistryStore registryStore; + private readonly IConfiguration configuration; + private readonly IDateTimeProvider dateTimeProvider; + private readonly TimeSpan timeoutPeriod; + private readonly int maxServerCount; - public ServerRegistryService(IServerRegistryStore registryStore) + public ServerRegistryService( + IServerRegistryStore registryStore, + IConfiguration configuration, + IDateTimeProvider dateTimeProvider) { this.registryStore = registryStore; + this.configuration = configuration; + this.dateTimeProvider = dateTimeProvider; + this.timeoutPeriod = TimeSpan.FromMinutes(Convert.ToDouble(this.configuration["ServerTimeoutPeriodInMinutes"])); + this.maxServerCount = Convert.ToInt32(this.configuration["MaxServerCount"]); } public async Task AddServer( - IPAddress ipAddress, ServerInfo server) { - ServerInfo existingServer = await this.registryStore.GetServerInfo(ipAddress ?? throw new ArgumentNullException(nameof(ipAddress))); - if (existingServer != null) + if (server == null) { - + throw new ArgumentNullException(nameof(server)); } - await this.registryStore.UpsertServerInfo(ipAddress, server ?? throw new ArgumentNullException(nameof(server))); + if (string.IsNullOrEmpty(server.HostIpAddress)) + { + throw new ArgumentException(nameof(server.HostIpAddress)); + } + + await this.registryStore.UpsertServerInfo(server ?? throw new ArgumentNullException(nameof(server))); } public async Task> GetAllServers() { - return await this.registryStore.GetAllServerInfos(); + return (await this.registryStore + .GetServerInfos(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 new file mode 100644 index 0000000..8c312ab --- /dev/null +++ b/Giants.Services/Store/CosmosDbClient.cs @@ -0,0 +1,135 @@ +namespace Giants.Services +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Linq.Expressions; + using System.Threading.Tasks; + using Giants.Services.Core.Entities; + using Microsoft.Azure.Cosmos; + using Microsoft.Azure.Cosmos.Linq; + + public class CosmosDbClient + { + private readonly string connectionString; + private readonly string authKeyOrResourceToken; + private readonly string databaseId; + private readonly string containerId; + private CosmosClient client; + private Container container; + + public CosmosDbClient( + string connectionString, + string authKeyOrResourceToken, + string databaseId, + string containerId) + { + this.connectionString = connectionString; + this.authKeyOrResourceToken = authKeyOrResourceToken; + this.databaseId = databaseId; + this.containerId = containerId; + } + + public async Task> GetItems( + Expression> selectExpression, + string partitionKey, + Expression> whereExpression = null) + where T : IIdentifiable + { + IQueryable query = this.container + .GetItemLinqQueryable(requestOptions: new QueryRequestOptions + { + PartitionKey = new PartitionKey(partitionKey), + }); + + if (whereExpression != null) + { + query = query.Where(whereExpression); + } + + var feedIteratorQuery = query + .Select(selectExpression) + .ToFeedIterator(); + + var items = new List(); + while (feedIteratorQuery.HasMoreResults) + { + var results = await feedIteratorQuery.ReadNextAsync(); + foreach (var result in results) + { + items.Add(result); + } + } + + return items; + } + + public async Task> GetItems( + Expression> whereExpression = null, + string partitionKey = null) + where T : IIdentifiable + { + if (partitionKey == null) + { + partitionKey = typeof(T).Name; + } + + IQueryable query = this.container + .GetItemLinqQueryable(requestOptions: new QueryRequestOptions + { + PartitionKey = new PartitionKey(partitionKey), + }); + + if (whereExpression != null) + { + query = query.Where(whereExpression); + } + + var feedIteratorQuery = query + .ToFeedIterator(); + + var items = new List(); + while (feedIteratorQuery.HasMoreResults) + { + var results = await feedIteratorQuery.ReadNextAsync(); + foreach (var result in results) + { + items.Add(result); + } + } + + return items; + } + + public async Task GetItemById(string id, string partitionKey = null) + where T : IIdentifiable + { + return (await this.GetItems(t => t.id == id, partitionKey)).FirstOrDefault(); + } + + public async Task UpsertItem( + T item, + PartitionKey? partitionKey = null, + ItemRequestOptions itemRequestOptions = null) + where T : IIdentifiable + { + await this.container.UpsertItemAsync(item, partitionKey, itemRequestOptions); + } + + public async Task Initialize(string partitionKeyPath = null) + { + this.client = new CosmosClient( + this.connectionString, + this.authKeyOrResourceToken); + + var databaseResponse = await this.client.CreateDatabaseIfNotExistsAsync(databaseId); + var containerResponse = await databaseResponse.Database.CreateContainerIfNotExistsAsync(new ContainerProperties() + { + Id = containerId, + PartitionKeyPath = partitionKeyPath ?? "/DocumentType" + }); + + this.container = containerResponse.Container; + } + } +} diff --git a/Giants.Services/Store/CosmosDbServerRegistryStore.cs b/Giants.Services/Store/CosmosDbServerRegistryStore.cs new file mode 100644 index 0000000..9521f8c --- /dev/null +++ b/Giants.Services/Store/CosmosDbServerRegistryStore.cs @@ -0,0 +1,47 @@ +namespace Giants.Services +{ + using System; + using System.Collections.Generic; + using System.Linq.Expressions; + using System.Threading.Tasks; + using Microsoft.Azure.Cosmos; + using Microsoft.Extensions.Configuration; + + public class CosmosDbServerRegistryStore : IServerRegistryStore + { + private readonly IConfiguration configuration; + private CosmosDbClient client; + + public CosmosDbServerRegistryStore(IConfiguration configuration) + { + this.configuration = configuration; + } + + public async Task> GetServerInfos( + Expression> whereExpression = null) + { + return await this.client.GetItems(whereExpression); + } + + public async Task GetServerInfo(string ipAddress) + { + return await this.client.GetItemById(ipAddress); + } + + public async Task UpsertServerInfo(ServerInfo serverInfo) + { + await this.client.UpsertItem(serverInfo, new PartitionKey(serverInfo.DocumentType)); + } + + public async Task Initialize() + { + this.client = new CosmosDbClient( + connectionString: this.configuration["CosmosDbEndpoint"], + authKeyOrResourceToken: this.configuration["CosmosDbKey"], + databaseId: this.configuration["DatabaseId"], + containerId: this.configuration["ContainerId"]); + + await this.client.Initialize(); + } + } +} diff --git a/Giants.Services/Services/FileUpdaterStore.cs b/Giants.Services/Store/FileUpdaterStore.cs similarity index 100% rename from Giants.Services/Services/FileUpdaterStore.cs rename to Giants.Services/Store/FileUpdaterStore.cs diff --git a/Giants.Services/Store/IServerRegistryStore.cs b/Giants.Services/Store/IServerRegistryStore.cs new file mode 100644 index 0000000..b54224e --- /dev/null +++ b/Giants.Services/Store/IServerRegistryStore.cs @@ -0,0 +1,18 @@ +namespace Giants.Services +{ + using System; + using System.Collections.Generic; + using System.Linq.Expressions; + using System.Threading.Tasks; + + public interface IServerRegistryStore + { + public Task Initialize(); + + public Task> GetServerInfos(Expression> whereExpression = null); + + public Task GetServerInfo(string ipAddress); + + public Task UpsertServerInfo(ServerInfo serverInfo); + } +} diff --git a/Giants.Services/Services/IUpdaterStore.cs b/Giants.Services/Store/IUpdaterStore.cs similarity index 100% rename from Giants.Services/Services/IUpdaterStore.cs rename to Giants.Services/Store/IUpdaterStore.cs diff --git a/Giants.Services/Services/InMemoryServerRegistryStore.cs b/Giants.Services/Store/InMemoryServerRegistryStore.cs similarity index 51% rename from Giants.Services/Services/InMemoryServerRegistryStore.cs rename to Giants.Services/Store/InMemoryServerRegistryStore.cs index 8e207f2..4c3683d 100644 --- a/Giants.Services/Services/InMemoryServerRegistryStore.cs +++ b/Giants.Services/Store/InMemoryServerRegistryStore.cs @@ -1,22 +1,25 @@ namespace Giants.Services { + using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; + using System.Linq.Expressions; using System.Net; using System.Threading.Tasks; public class InMemoryServerRegistryStore : IServerRegistryStore { - private ConcurrentDictionary servers = new ConcurrentDictionary(); + private ConcurrentDictionary servers = new ConcurrentDictionary(); - public Task> GetAllServerInfos() + public Task> GetServerInfos( + Expression> whereExpression = null) { return Task.FromResult( this.servers.Values.AsEnumerable()); } - public Task GetServerInfo(IPAddress ipAddress) + public Task GetServerInfo(string ipAddress) { if (servers.ContainsKey(ipAddress)) { @@ -26,9 +29,16 @@ return Task.FromResult((ServerInfo)null); } - public Task UpsertServerInfo(IPAddress ipAddress, ServerInfo serverInfo) + public Task Initialize() { - servers.TryAdd(ipAddress, serverInfo); + this.servers.Clear(); + + return Task.CompletedTask; + } + + public Task UpsertServerInfo(ServerInfo serverInfo) + { + this.servers.TryAdd(serverInfo.HostIpAddress, serverInfo); return Task.CompletedTask; } diff --git a/Giants.WebApi/Controllers/ServersController.cs b/Giants.WebApi/Controllers/ServersController.cs index c5da69d..ba3bb9c 100644 --- a/Giants.WebApi/Controllers/ServersController.cs +++ b/Giants.WebApi/Controllers/ServersController.cs @@ -59,7 +59,7 @@ namespace Giants.Web.Controllers serverInfoEntity.HostIpAddress = requestIpAddress.ToString(); serverInfoEntity.LastHeartbeat = DateTime.UtcNow; - await this.serverRegistryService.AddServer(requestIpAddress, serverInfoEntity); + await this.serverRegistryService.AddServer(serverInfoEntity); } } } diff --git a/Giants.WebApi/InitializerHostedService.cs b/Giants.WebApi/InitializerHostedService.cs new file mode 100644 index 0000000..b6ff7d0 --- /dev/null +++ b/Giants.WebApi/InitializerHostedService.cs @@ -0,0 +1,30 @@ +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 +{ + public class InitializerHostedService : IHostedService + { + private readonly IServerRegistryStore serverRegistryStore; + + public InitializerHostedService(IServerRegistryStore serverRegistryStore) + { + this.serverRegistryStore = serverRegistryStore; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + await this.serverRegistryStore.Initialize(); + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + } +} diff --git a/Giants.WebApi/Program.cs b/Giants.WebApi/Program.cs index 47c2e2b..05a43a5 100644 --- a/Giants.WebApi/Program.cs +++ b/Giants.WebApi/Program.cs @@ -1,14 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; - namespace Giants.Web { + using Microsoft.AspNetCore.Hosting; + using Microsoft.Extensions.Hosting; + public class Program { public static void Main(string[] args) diff --git a/Giants.WebApi/Startup.cs b/Giants.WebApi/Startup.cs index 317052d..1be2929 100644 --- a/Giants.WebApi/Startup.cs +++ b/Giants.WebApi/Startup.cs @@ -1,5 +1,6 @@ using AutoMapper; using Giants.Services; +using Giants.WebApi; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Infrastructure; @@ -26,8 +27,9 @@ namespace Giants.Web services.AddHttpContextAccessor(); services.TryAddSingleton(); + services.AddHostedService(); - ServicesModule.RegisterServices(services); + ServicesModule.RegisterServices(services, Configuration); IMapper mapper = Services.Mapper.GetMapper(); services.AddSingleton(mapper); diff --git a/Giants.WebApi/appsettings.Development.json b/Giants.WebApi/appsettings.Development.json deleted file mode 100644 index 8983e0f..0000000 --- a/Giants.WebApi/appsettings.Development.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - } -} diff --git a/Giants.WebApi/appsettings.json b/Giants.WebApi/appsettings.json index d9d9a9b..ca3573c 100644 --- a/Giants.WebApi/appsettings.json +++ b/Giants.WebApi/appsettings.json @@ -6,5 +6,9 @@ "Microsoft.Hosting.Lifetime": "Information" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "DatabaseId": "DefaultDatabase", + "ContainerId": "DefaultContainer", + "ServerTimeoutPeriodInMinutes": "7", + "MaxServerCount": 1000 }