From 26080cdb60f1a25a789d4c3feaaef1e9c4df1c7e Mon Sep 17 00:00:00 2001 From: Tom Pallister Date: Sun, 12 Aug 2018 18:22:05 +0100 Subject: [PATCH 1/3] half way done, waiting on ocelot pre release version for some testing --- src/Ocelot.Provider.Rafty/BearerToken.cs | 16 + src/Ocelot.Provider.Rafty/FakeCommand.cs | 14 + src/Ocelot.Provider.Rafty/FilePeer.cs | 7 + src/Ocelot.Provider.Rafty/FilePeers.cs | 14 + .../FilePeersProvider.cs | 44 ++ src/Ocelot.Provider.Rafty/HttpPeer.cs | 129 +++++ .../Ocelot.Provider.Rafty.csproj | 3 +- .../OcelotAdministrationBuilderExtensions.cs | 26 + .../OcelotBuilderExtensions.cs | 12 - .../OcelotFiniteStateMachine.cs | 25 + src/Ocelot.Provider.Rafty/RaftController.cs | 96 ++++ .../RaftyFileConfigurationSetter.cs | 30 ++ .../RaftyMiddlewareConfigurationProvider.cs | 49 ++ src/Ocelot.Provider.Rafty/SqlLiteLog.cs | 334 ++++++++++++ .../UnableToSaveAcceptCommand.cs | 11 + .../UpdateFileConfiguration.cs | 15 + ...elot.Provider.Rafty.AcceptanceTests.csproj | 2 +- .../Ocelot.Provider.Rafty.Benchmarks.csproj | 3 +- .../BearerToken.cs | 16 + ...lot.Provider.Rafty.IntegrationTests.csproj | 2 +- .../RaftTests.cs | 510 ++++++++++++++++++ .../Ocelot.Provider.Rafty.ManualTest.csproj | 3 +- .../Ocelot.Provider.Rafty.UnitTests.csproj | 2 +- ...lotAdministrationBuilderExtensionsTests.cs | 76 +++ .../OcelotFiniteStateMachineTests.cs | 44 ++ .../RaftyFileConfigurationSetterTests.cs | 51 ++ 26 files changed, 1516 insertions(+), 18 deletions(-) create mode 100644 src/Ocelot.Provider.Rafty/BearerToken.cs create mode 100644 src/Ocelot.Provider.Rafty/FakeCommand.cs create mode 100644 src/Ocelot.Provider.Rafty/FilePeer.cs create mode 100644 src/Ocelot.Provider.Rafty/FilePeers.cs create mode 100644 src/Ocelot.Provider.Rafty/FilePeersProvider.cs create mode 100644 src/Ocelot.Provider.Rafty/HttpPeer.cs create mode 100644 src/Ocelot.Provider.Rafty/OcelotAdministrationBuilderExtensions.cs delete mode 100644 src/Ocelot.Provider.Rafty/OcelotBuilderExtensions.cs create mode 100644 src/Ocelot.Provider.Rafty/OcelotFiniteStateMachine.cs create mode 100644 src/Ocelot.Provider.Rafty/RaftController.cs create mode 100644 src/Ocelot.Provider.Rafty/RaftyFileConfigurationSetter.cs create mode 100644 src/Ocelot.Provider.Rafty/RaftyMiddlewareConfigurationProvider.cs create mode 100644 src/Ocelot.Provider.Rafty/SqlLiteLog.cs create mode 100644 src/Ocelot.Provider.Rafty/UnableToSaveAcceptCommand.cs create mode 100644 src/Ocelot.Provider.Rafty/UpdateFileConfiguration.cs create mode 100644 test/Ocelot.Provider.Rafty.IntegrationTests/BearerToken.cs create mode 100644 test/Ocelot.Provider.Rafty.IntegrationTests/RaftTests.cs create mode 100644 test/Ocelot.Provider.Rafty.UnitTests/OcelotAdministrationBuilderExtensionsTests.cs create mode 100644 test/Ocelot.Provider.Rafty.UnitTests/OcelotFiniteStateMachineTests.cs create mode 100644 test/Ocelot.Provider.Rafty.UnitTests/RaftyFileConfigurationSetterTests.cs diff --git a/src/Ocelot.Provider.Rafty/BearerToken.cs b/src/Ocelot.Provider.Rafty/BearerToken.cs new file mode 100644 index 0000000..c006aaf --- /dev/null +++ b/src/Ocelot.Provider.Rafty/BearerToken.cs @@ -0,0 +1,16 @@ +namespace Ocelot.Provider.Rafty +{ + using Newtonsoft.Json; + + internal class BearerToken + { + [JsonProperty("access_token")] + public string AccessToken { get; set; } + + [JsonProperty("expires_in")] + public int ExpiresIn { get; set; } + + [JsonProperty("token_type")] + public string TokenType { get; set; } + } +} diff --git a/src/Ocelot.Provider.Rafty/FakeCommand.cs b/src/Ocelot.Provider.Rafty/FakeCommand.cs new file mode 100644 index 0000000..de611da --- /dev/null +++ b/src/Ocelot.Provider.Rafty/FakeCommand.cs @@ -0,0 +1,14 @@ +namespace Ocelot.Provider.Rafty +{ + using global::Rafty.FiniteStateMachine; + + public class FakeCommand : ICommand + { + public FakeCommand(string value) + { + this.Value = value; + } + + public string Value { get; private set; } + } +} diff --git a/src/Ocelot.Provider.Rafty/FilePeer.cs b/src/Ocelot.Provider.Rafty/FilePeer.cs new file mode 100644 index 0000000..4bb5754 --- /dev/null +++ b/src/Ocelot.Provider.Rafty/FilePeer.cs @@ -0,0 +1,7 @@ +namespace Ocelot.Provider.Rafty +{ + public class FilePeer + { + public string HostAndPort { get; set; } + } +} diff --git a/src/Ocelot.Provider.Rafty/FilePeers.cs b/src/Ocelot.Provider.Rafty/FilePeers.cs new file mode 100644 index 0000000..4d5f9e3 --- /dev/null +++ b/src/Ocelot.Provider.Rafty/FilePeers.cs @@ -0,0 +1,14 @@ +namespace Ocelot.Provider.Rafty +{ + using System.Collections.Generic; + + public class FilePeers + { + public FilePeers() + { + Peers = new List(); + } + + public List Peers { get; set; } + } +} diff --git a/src/Ocelot.Provider.Rafty/FilePeersProvider.cs b/src/Ocelot.Provider.Rafty/FilePeersProvider.cs new file mode 100644 index 0000000..15f42df --- /dev/null +++ b/src/Ocelot.Provider.Rafty/FilePeersProvider.cs @@ -0,0 +1,44 @@ +namespace Ocelot.Provider.Rafty +{ + using System.Net.Http; + using Configuration; + using Configuration.Repository; + using global::Rafty.Concensus.Peers; + using global::Rafty.Infrastructure; + using Microsoft.Extensions.Options; + using Middleware; + using System.Collections.Generic; + + public class FilePeersProvider : IPeersProvider + { + private readonly IOptions _options; + private readonly List _peers; + private IBaseUrlFinder _finder; + private IInternalConfigurationRepository _repo; + private IIdentityServerConfiguration _identityServerConfig; + + public FilePeersProvider(IOptions options, IBaseUrlFinder finder, IInternalConfigurationRepository repo, IIdentityServerConfiguration identityServerConfig) + { + _identityServerConfig = identityServerConfig; + _repo = repo; + _finder = finder; + _options = options; + _peers = new List(); + + var config = _repo.Get(); + foreach (var item in _options.Value.Peers) + { + var httpClient = new HttpClient(); + + //todo what if this errors? + var httpPeer = new HttpPeer(item.HostAndPort, httpClient, _finder, config.Data, _identityServerConfig); + _peers.Add(httpPeer); + } + } + + public List Get() + { + return _peers; + } + } +} diff --git a/src/Ocelot.Provider.Rafty/HttpPeer.cs b/src/Ocelot.Provider.Rafty/HttpPeer.cs new file mode 100644 index 0000000..b46a856 --- /dev/null +++ b/src/Ocelot.Provider.Rafty/HttpPeer.cs @@ -0,0 +1,129 @@ +namespace Ocelot.Provider.Rafty +{ + using System.Net.Http; + using System.Threading.Tasks; + using Configuration; + using global::Rafty.Concensus.Messages; + using global::Rafty.Concensus.Peers; + using global::Rafty.FiniteStateMachine; + using global::Rafty.Infrastructure; + using Middleware; + using Newtonsoft.Json; + using System; + using System.Collections.Generic; + + public class HttpPeer : IPeer + { + private readonly string _hostAndPort; + private readonly HttpClient _httpClient; + private readonly JsonSerializerSettings _jsonSerializerSettings; + private readonly string _baseSchemeUrlAndPort; + private BearerToken _token; + private readonly IInternalConfiguration _config; + private readonly IIdentityServerConfiguration _identityServerConfiguration; + + public HttpPeer(string hostAndPort, HttpClient httpClient, IBaseUrlFinder finder, IInternalConfiguration config, IIdentityServerConfiguration identityServerConfiguration) + { + _identityServerConfiguration = identityServerConfiguration; + _config = config; + Id = hostAndPort; + _hostAndPort = hostAndPort; + _httpClient = httpClient; + _jsonSerializerSettings = new JsonSerializerSettings() + { + TypeNameHandling = TypeNameHandling.All + }; + _baseSchemeUrlAndPort = finder.Find(); + } + + public string Id { get; } + + public async Task Request(RequestVote requestVote) + { + if (_token == null) + { + await SetToken(); + } + + var json = JsonConvert.SerializeObject(requestVote, _jsonSerializerSettings); + var content = new StringContent(json); + content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); + var response = await _httpClient.PostAsync($"{_hostAndPort}/administration/raft/requestvote", content); + if (response.IsSuccessStatusCode) + { + return JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync(), _jsonSerializerSettings); + } + + return new RequestVoteResponse(false, requestVote.Term); + } + + public async Task Request(AppendEntries appendEntries) + { + try + { + if (_token == null) + { + await SetToken(); + } + + var json = JsonConvert.SerializeObject(appendEntries, _jsonSerializerSettings); + var content = new StringContent(json); + content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); + var response = await _httpClient.PostAsync($"{_hostAndPort}/administration/raft/appendEntries", content); + if (response.IsSuccessStatusCode) + { + return JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync(), _jsonSerializerSettings); + } + + return new AppendEntriesResponse(appendEntries.Term, false); + } + catch (Exception ex) + { + Console.WriteLine(ex); + return new AppendEntriesResponse(appendEntries.Term, false); + } + } + + public async Task> Request(T command) + where T : ICommand + { + Console.WriteLine("SENDING REQUEST...."); + if (_token == null) + { + await SetToken(); + } + + var json = JsonConvert.SerializeObject(command, _jsonSerializerSettings); + var content = new StringContent(json); + content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); + var response = await _httpClient.PostAsync($"{_hostAndPort}/administration/raft/command", content); + if (response.IsSuccessStatusCode) + { + Console.WriteLine("REQUEST OK...."); + var okResponse = JsonConvert.DeserializeObject>(await response.Content.ReadAsStringAsync(), _jsonSerializerSettings); + return new OkResponse((T)okResponse.Command); + } + + Console.WriteLine("REQUEST NOT OK...."); + return new ErrorResponse(await response.Content.ReadAsStringAsync(), command); + } + + private async Task SetToken() + { + var tokenUrl = $"{_baseSchemeUrlAndPort}{_config.AdministrationPath}/connect/token"; + var formData = new List> + { + new KeyValuePair("client_id", _identityServerConfiguration.ApiName), + new KeyValuePair("client_secret", _identityServerConfiguration.ApiSecret), + new KeyValuePair("scope", _identityServerConfiguration.ApiName), + new KeyValuePair("grant_type", "client_credentials") + }; + var content = new FormUrlEncodedContent(formData); + var response = await _httpClient.PostAsync(tokenUrl, content); + var responseContent = await response.Content.ReadAsStringAsync(); + response.EnsureSuccessStatusCode(); + _token = JsonConvert.DeserializeObject(responseContent); + _httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue(_token.TokenType, _token.AccessToken); + } + } +} diff --git a/src/Ocelot.Provider.Rafty/Ocelot.Provider.Rafty.csproj b/src/Ocelot.Provider.Rafty/Ocelot.Provider.Rafty.csproj index 634c3dd..ca85214 100644 --- a/src/Ocelot.Provider.Rafty/Ocelot.Provider.Rafty.csproj +++ b/src/Ocelot.Provider.Rafty/Ocelot.Provider.Rafty.csproj @@ -26,7 +26,8 @@ True - + + all diff --git a/src/Ocelot.Provider.Rafty/OcelotAdministrationBuilderExtensions.cs b/src/Ocelot.Provider.Rafty/OcelotAdministrationBuilderExtensions.cs new file mode 100644 index 0000000..8a94adc --- /dev/null +++ b/src/Ocelot.Provider.Rafty/OcelotAdministrationBuilderExtensions.cs @@ -0,0 +1,26 @@ +namespace Ocelot.Provider.Rafty +{ + using Configuration.Setter; + using DependencyInjection; + using global::Rafty.Concensus.Node; + using global::Rafty.FiniteStateMachine; + using global::Rafty.Infrastructure; + using global::Rafty.Log; + + public static class OcelotAdministrationBuilderExtensions + { + public static IOcelotAdministrationBuilder AddRafty(this IOcelotAdministrationBuilder builder) + { + var settings = new InMemorySettings(4000, 6000, 100, 10000); + builder.Services.RemoveAll(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(settings); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.Configure(builder.ConfigurationRoot); + return builder; + } + } +} diff --git a/src/Ocelot.Provider.Rafty/OcelotBuilderExtensions.cs b/src/Ocelot.Provider.Rafty/OcelotBuilderExtensions.cs deleted file mode 100644 index 697841e..0000000 --- a/src/Ocelot.Provider.Rafty/OcelotBuilderExtensions.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Ocelot.Provider.Rafty -{ - using DependencyInjection; - - public static class OcelotBuilderExtensions - { - public static IOcelotBuilder AddSomething(this IOcelotBuilder builder) - { - return builder; - } - } -} diff --git a/src/Ocelot.Provider.Rafty/OcelotFiniteStateMachine.cs b/src/Ocelot.Provider.Rafty/OcelotFiniteStateMachine.cs new file mode 100644 index 0000000..c7dd3ef --- /dev/null +++ b/src/Ocelot.Provider.Rafty/OcelotFiniteStateMachine.cs @@ -0,0 +1,25 @@ +namespace Ocelot.Provider.Rafty +{ + using System.Threading.Tasks; + using Configuration.Setter; + using global::Rafty.FiniteStateMachine; + using global::Rafty.Log; + + public class OcelotFiniteStateMachine : IFiniteStateMachine + { + private readonly IFileConfigurationSetter _setter; + + public OcelotFiniteStateMachine(IFileConfigurationSetter setter) + { + _setter = setter; + } + + public async Task Handle(LogEntry log) + { + //todo - handle an error + //hack it to just cast as at the moment we know this is the only command :P + var hack = (UpdateFileConfiguration)log.CommandData; + await _setter.Set(hack.Configuration); + } + } +} diff --git a/src/Ocelot.Provider.Rafty/RaftController.cs b/src/Ocelot.Provider.Rafty/RaftController.cs new file mode 100644 index 0000000..9cf51dd --- /dev/null +++ b/src/Ocelot.Provider.Rafty/RaftController.cs @@ -0,0 +1,96 @@ +namespace Ocelot.Provider.Rafty +{ + using System; + using System.IO; + using System.Threading.Tasks; + using global::Rafty.Concensus.Messages; + using global::Rafty.Concensus.Node; + using global::Rafty.FiniteStateMachine; + using Logging; + using Microsoft.AspNetCore.Authorization; + using Microsoft.AspNetCore.Mvc; + using Middleware; + using Newtonsoft.Json; + + [Authorize] + [Route("raft")] + public class RaftController : Controller + { + private readonly INode _node; + private readonly IOcelotLogger _logger; + private readonly string _baseSchemeUrlAndPort; + private readonly JsonSerializerSettings _jsonSerialiserSettings; + + public RaftController(INode node, IOcelotLoggerFactory loggerFactory, IBaseUrlFinder finder) + { + _jsonSerialiserSettings = new JsonSerializerSettings + { + TypeNameHandling = TypeNameHandling.All + }; + _baseSchemeUrlAndPort = finder.Find(); + _logger = loggerFactory.CreateLogger(); + _node = node; + } + + [Route("appendentries")] + public async Task AppendEntries() + { + using (var reader = new StreamReader(HttpContext.Request.Body)) + { + var json = await reader.ReadToEndAsync(); + + var appendEntries = JsonConvert.DeserializeObject(json, _jsonSerialiserSettings); + + _logger.LogDebug($"{_baseSchemeUrlAndPort}/appendentries called, my state is {_node.State.GetType().FullName}"); + + var appendEntriesResponse = await _node.Handle(appendEntries); + + return new OkObjectResult(appendEntriesResponse); + } + } + + [Route("requestvote")] + public async Task RequestVote() + { + using (var reader = new StreamReader(HttpContext.Request.Body)) + { + var json = await reader.ReadToEndAsync(); + + var requestVote = JsonConvert.DeserializeObject(json, _jsonSerialiserSettings); + + _logger.LogDebug($"{_baseSchemeUrlAndPort}/requestvote called, my state is {_node.State.GetType().FullName}"); + + var requestVoteResponse = await _node.Handle(requestVote); + + return new OkObjectResult(requestVoteResponse); + } + } + + [Route("command")] + public async Task Command() + { + try + { + using (var reader = new StreamReader(HttpContext.Request.Body)) + { + var json = await reader.ReadToEndAsync(); + + var command = JsonConvert.DeserializeObject(json, _jsonSerialiserSettings); + + _logger.LogDebug($"{_baseSchemeUrlAndPort}/command called, my state is {_node.State.GetType().FullName}"); + + var commandResponse = await _node.Accept(command); + + json = JsonConvert.SerializeObject(commandResponse, _jsonSerialiserSettings); + + return StatusCode(200, json); + } + } + catch (Exception e) + { + _logger.LogError($"THERE WAS A PROBLEM ON NODE {_node.State.CurrentState.Id}", e); + throw; + } + } + } +} diff --git a/src/Ocelot.Provider.Rafty/RaftyFileConfigurationSetter.cs b/src/Ocelot.Provider.Rafty/RaftyFileConfigurationSetter.cs new file mode 100644 index 0000000..bf26ab0 --- /dev/null +++ b/src/Ocelot.Provider.Rafty/RaftyFileConfigurationSetter.cs @@ -0,0 +1,30 @@ +namespace Ocelot.Provider.Rafty +{ + using System.Threading.Tasks; + using Configuration.File; + using Configuration.Setter; + using global::Rafty.Concensus.Node; + using global::Rafty.Infrastructure; + + public class RaftyFileConfigurationSetter : IFileConfigurationSetter + { + private readonly INode _node; + + public RaftyFileConfigurationSetter(INode node) + { + _node = node; + } + + public async Task Set(FileConfiguration fileConfiguration) + { + var result = await _node.Accept(new UpdateFileConfiguration(fileConfiguration)); + + if (result.GetType() == typeof(ErrorResponse)) + { + return new Responses.ErrorResponse(new UnableToSaveAcceptCommand($"unable to save file configuration to state machine")); + } + + return new Responses.OkResponse(); + } + } +} diff --git a/src/Ocelot.Provider.Rafty/RaftyMiddlewareConfigurationProvider.cs b/src/Ocelot.Provider.Rafty/RaftyMiddlewareConfigurationProvider.cs new file mode 100644 index 0000000..e8c3ad5 --- /dev/null +++ b/src/Ocelot.Provider.Rafty/RaftyMiddlewareConfigurationProvider.cs @@ -0,0 +1,49 @@ +namespace Ocelot.Provider.Rafty +{ + using System.Threading.Tasks; + using global::Rafty.Concensus.Node; + using global::Rafty.Infrastructure; + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; + using Middleware; + using Microsoft.AspNetCore.Hosting; + + public static class RaftyMiddlewareConfigurationProvider + { + public static OcelotMiddlewareConfigurationDelegate Get = builder => + { + if (UsingRafty(builder)) + { + SetUpRafty(builder); + } + + return Task.CompletedTask; + }; + + private static bool UsingRafty(IApplicationBuilder builder) + { + var node = builder.ApplicationServices.GetService(); + if (node != null) + { + return true; + } + + return false; + } + + private static void SetUpRafty(IApplicationBuilder builder) + { + var applicationLifetime = builder.ApplicationServices.GetService(); + applicationLifetime.ApplicationStopping.Register(() => OnShutdown(builder)); + var node = builder.ApplicationServices.GetService(); + var nodeId = builder.ApplicationServices.GetService(); + node.Start(nodeId); + } + + private static void OnShutdown(IApplicationBuilder app) + { + var node = app.ApplicationServices.GetService(); + node.Stop(); + } + } +} diff --git a/src/Ocelot.Provider.Rafty/SqlLiteLog.cs b/src/Ocelot.Provider.Rafty/SqlLiteLog.cs new file mode 100644 index 0000000..618b485 --- /dev/null +++ b/src/Ocelot.Provider.Rafty/SqlLiteLog.cs @@ -0,0 +1,334 @@ +namespace Ocelot.Provider.Rafty +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Threading; + using System.Threading.Tasks; + using global::Rafty.Infrastructure; + using global::Rafty.Log; + using Microsoft.Data.Sqlite; + using Microsoft.Extensions.Logging; + using Newtonsoft.Json; + + public class SqlLiteLog : ILog + { + private readonly string _path; + private readonly SemaphoreSlim _sempaphore = new SemaphoreSlim(1, 1); + private readonly ILogger _logger; + private readonly NodeId _nodeId; + + public SqlLiteLog(NodeId nodeId, ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + _nodeId = nodeId; + _path = $"{nodeId.Id.Replace("/", "").Replace(":", "")}.db"; + _sempaphore.Wait(); + + if (!File.Exists(_path)) + { + var fs = File.Create(_path); + + fs.Dispose(); + + using (var connection = new SqliteConnection($"Data Source={_path};")) + { + connection.Open(); + + const string sql = @"create table logs ( + id integer primary key, + data text not null + )"; + + using (var command = new SqliteCommand(sql, connection)) + { + var result = command.ExecuteNonQuery(); + + _logger.LogInformation(result == 0 + ? $"id: {_nodeId.Id} create database, result: {result}" + : $"id: {_nodeId.Id} did not create database., result: {result}"); + } + } + } + + _sempaphore.Release(); + } + + public async Task LastLogIndex() + { + _sempaphore.Wait(); + var result = 1; + using (var connection = new SqliteConnection($"Data Source={_path};")) + { + connection.Open(); + var sql = @"select id from logs order by id desc limit 1"; + using (var command = new SqliteCommand(sql, connection)) + { + var index = Convert.ToInt32(await command.ExecuteScalarAsync()); + if (index > result) + { + result = index; + } + } + } + + _sempaphore.Release(); + return result; + } + + public async Task LastLogTerm() + { + _sempaphore.Wait(); + long result = 0; + using (var connection = new SqliteConnection($"Data Source={_path};")) + { + connection.Open(); + var sql = @"select data from logs order by id desc limit 1"; + using (var command = new SqliteCommand(sql, connection)) + { + var data = Convert.ToString(await command.ExecuteScalarAsync()); + var jsonSerializerSettings = new JsonSerializerSettings() + { + TypeNameHandling = TypeNameHandling.All + }; + var log = JsonConvert.DeserializeObject(data, jsonSerializerSettings); + if (log != null && log.Term > result) + { + result = log.Term; + } + } + } + + _sempaphore.Release(); + return result; + } + + public async Task Count() + { + _sempaphore.Wait(); + var result = 0; + using (var connection = new SqliteConnection($"Data Source={_path};")) + { + connection.Open(); + var sql = @"select count(id) from logs"; + using (var command = new SqliteCommand(sql, connection)) + { + var index = Convert.ToInt32(await command.ExecuteScalarAsync()); + if (index > result) + { + result = index; + } + } + } + + _sempaphore.Release(); + return result; + } + + public async Task Apply(LogEntry log) + { + _sempaphore.Wait(); + using (var connection = new SqliteConnection($"Data Source={_path};")) + { + connection.Open(); + var jsonSerializerSettings = new JsonSerializerSettings() + { + TypeNameHandling = TypeNameHandling.All + }; + var data = JsonConvert.SerializeObject(log, jsonSerializerSettings); + + //todo - sql injection dont copy this.. + var sql = $"insert into logs (data) values ('{data}')"; + _logger.LogInformation($"id: {_nodeId.Id}, sql: {sql}"); + using (var command = new SqliteCommand(sql, connection)) + { + var result = await command.ExecuteNonQueryAsync(); + _logger.LogInformation($"id: {_nodeId.Id}, insert log result: {result}"); + } + + sql = "select last_insert_rowid()"; + using (var command = new SqliteCommand(sql, connection)) + { + var result = await command.ExecuteScalarAsync(); + _logger.LogInformation($"id: {_nodeId.Id}, about to release semaphore"); + _sempaphore.Release(); + _logger.LogInformation($"id: {_nodeId.Id}, saved log to sqlite"); + return Convert.ToInt32(result); + } + } + } + + public async Task DeleteConflictsFromThisLog(int index, LogEntry logEntry) + { + _sempaphore.Wait(); + using (var connection = new SqliteConnection($"Data Source={_path};")) + { + connection.Open(); + + //todo - sql injection dont copy this.. + var sql = $"select data from logs where id = {index};"; + _logger.LogInformation($"id: {_nodeId.Id} sql: {sql}"); + using (var command = new SqliteCommand(sql, connection)) + { + var data = Convert.ToString(await command.ExecuteScalarAsync()); + var jsonSerializerSettings = new JsonSerializerSettings() + { + TypeNameHandling = TypeNameHandling.All + }; + + _logger.LogInformation($"id {_nodeId.Id} got log for index: {index}, data is {data} and new log term is {logEntry.Term}"); + + var log = JsonConvert.DeserializeObject(data, jsonSerializerSettings); + if (logEntry != null && log != null && logEntry.Term != log.Term) + { + //todo - sql injection dont copy this.. + var deleteSql = $"delete from logs where id >= {index};"; + _logger.LogInformation($"id: {_nodeId.Id} sql: {deleteSql}"); + using (var deleteCommand = new SqliteCommand(deleteSql, connection)) + { + var result = await deleteCommand.ExecuteNonQueryAsync(); + } + } + } + } + + _sempaphore.Release(); + } + + public async Task IsDuplicate(int index, LogEntry logEntry) + { + _sempaphore.Wait(); + using (var connection = new SqliteConnection($"Data Source={_path};")) + { + connection.Open(); + + //todo - sql injection dont copy this.. + var sql = $"select data from logs where id = {index};"; + using (var command = new SqliteCommand(sql, connection)) + { + var data = Convert.ToString(await command.ExecuteScalarAsync()); + var jsonSerializerSettings = new JsonSerializerSettings() + { + TypeNameHandling = TypeNameHandling.All + }; + + var log = JsonConvert.DeserializeObject(data, jsonSerializerSettings); + + if (logEntry != null && log != null && logEntry.Term == log.Term) + { + _sempaphore.Release(); + return true; + } + } + } + + _sempaphore.Release(); + return false; + } + + public async Task Get(int index) + { + _sempaphore.Wait(); + using (var connection = new SqliteConnection($"Data Source={_path};")) + { + connection.Open(); + + //todo - sql injection dont copy this.. + var sql = $"select data from logs where id = {index}"; + using (var command = new SqliteCommand(sql, connection)) + { + var data = Convert.ToString(await command.ExecuteScalarAsync()); + var jsonSerializerSettings = new JsonSerializerSettings() + { + TypeNameHandling = TypeNameHandling.All + }; + var log = JsonConvert.DeserializeObject(data, jsonSerializerSettings); + _sempaphore.Release(); + return log; + } + } + } + + public async Task> GetFrom(int index) + { + _sempaphore.Wait(); + var logsToReturn = new List<(int, LogEntry)>(); + + using (var connection = new SqliteConnection($"Data Source={_path};")) + { + connection.Open(); + + //todo - sql injection dont copy this.. + var sql = $"select id, data from logs where id >= {index}"; + using (var command = new SqliteCommand(sql, connection)) + { + using (var reader = await command.ExecuteReaderAsync()) + { + while (reader.Read()) + { + var id = Convert.ToInt32(reader[0]); + var data = (string)reader[1]; + var jsonSerializerSettings = new JsonSerializerSettings() + { + TypeNameHandling = TypeNameHandling.All + }; + var log = JsonConvert.DeserializeObject(data, jsonSerializerSettings); + logsToReturn.Add((id, log)); + } + } + } + + _sempaphore.Release(); + return logsToReturn; + } + } + + public async Task GetTermAtIndex(int index) + { + _sempaphore.Wait(); + long result = 0; + using (var connection = new SqliteConnection($"Data Source={_path};")) + { + connection.Open(); + + //todo - sql injection dont copy this.. + var sql = $"select data from logs where id = {index}"; + using (var command = new SqliteCommand(sql, connection)) + { + var data = Convert.ToString(await command.ExecuteScalarAsync()); + var jsonSerializerSettings = new JsonSerializerSettings() + { + TypeNameHandling = TypeNameHandling.All + }; + var log = JsonConvert.DeserializeObject(data, jsonSerializerSettings); + if (log != null && log.Term > result) + { + result = log.Term; + } + } + } + + _sempaphore.Release(); + return result; + } + + public async Task Remove(int indexOfCommand) + { + _sempaphore.Wait(); + using (var connection = new SqliteConnection($"Data Source={_path};")) + { + connection.Open(); + + //todo - sql injection dont copy this.. + var deleteSql = $"delete from logs where id >= {indexOfCommand};"; + _logger.LogInformation($"id: {_nodeId.Id} Remove {deleteSql}"); + using (var deleteCommand = new SqliteCommand(deleteSql, connection)) + { + var result = await deleteCommand.ExecuteNonQueryAsync(); + } + } + + _sempaphore.Release(); + } + } +} diff --git a/src/Ocelot.Provider.Rafty/UnableToSaveAcceptCommand.cs b/src/Ocelot.Provider.Rafty/UnableToSaveAcceptCommand.cs new file mode 100644 index 0000000..888987b --- /dev/null +++ b/src/Ocelot.Provider.Rafty/UnableToSaveAcceptCommand.cs @@ -0,0 +1,11 @@ +namespace Ocelot.Provider.Rafty +{ + using Errors; + public class UnableToSaveAcceptCommand : Error + { + public UnableToSaveAcceptCommand(string message) + : base(message, OcelotErrorCode.UnknownError) + { + } + } +} diff --git a/src/Ocelot.Provider.Rafty/UpdateFileConfiguration.cs b/src/Ocelot.Provider.Rafty/UpdateFileConfiguration.cs new file mode 100644 index 0000000..894a758 --- /dev/null +++ b/src/Ocelot.Provider.Rafty/UpdateFileConfiguration.cs @@ -0,0 +1,15 @@ +namespace Ocelot.Provider.Rafty +{ + using Configuration.File; + using global::Rafty.FiniteStateMachine; + + public class UpdateFileConfiguration : ICommand + { + public UpdateFileConfiguration(FileConfiguration configuration) + { + Configuration = configuration; + } + + public FileConfiguration Configuration { get; private set; } + } +} diff --git a/test/Ocelot.Provider.Rafty.AcceptanceTests/Ocelot.Provider.Rafty.AcceptanceTests.csproj b/test/Ocelot.Provider.Rafty.AcceptanceTests/Ocelot.Provider.Rafty.AcceptanceTests.csproj index 0de4474..a8c4d16 100644 --- a/test/Ocelot.Provider.Rafty.AcceptanceTests/Ocelot.Provider.Rafty.AcceptanceTests.csproj +++ b/test/Ocelot.Provider.Rafty.AcceptanceTests/Ocelot.Provider.Rafty.AcceptanceTests.csproj @@ -33,7 +33,7 @@ - + all diff --git a/test/Ocelot.Provider.Rafty.Benchmarks/Ocelot.Provider.Rafty.Benchmarks.csproj b/test/Ocelot.Provider.Rafty.Benchmarks/Ocelot.Provider.Rafty.Benchmarks.csproj index 1c920b1..cecc29a 100644 --- a/test/Ocelot.Provider.Rafty.Benchmarks/Ocelot.Provider.Rafty.Benchmarks.csproj +++ b/test/Ocelot.Provider.Rafty.Benchmarks/Ocelot.Provider.Rafty.Benchmarks.csproj @@ -15,7 +15,8 @@ - + + all diff --git a/test/Ocelot.Provider.Rafty.IntegrationTests/BearerToken.cs b/test/Ocelot.Provider.Rafty.IntegrationTests/BearerToken.cs new file mode 100644 index 0000000..aee5cf8 --- /dev/null +++ b/test/Ocelot.Provider.Rafty.IntegrationTests/BearerToken.cs @@ -0,0 +1,16 @@ +namespace Ocelot.Provider.Rafty.IntegrationTests +{ + using Newtonsoft.Json; + + class BearerToken + { + [JsonProperty("access_token")] + public string AccessToken { get; set; } + + [JsonProperty("expires_in")] + public int ExpiresIn { get; set; } + + [JsonProperty("token_type")] + public string TokenType { get; set; } + } +} diff --git a/test/Ocelot.Provider.Rafty.IntegrationTests/Ocelot.Provider.Rafty.IntegrationTests.csproj b/test/Ocelot.Provider.Rafty.IntegrationTests/Ocelot.Provider.Rafty.IntegrationTests.csproj index e4160e3..bc5d5f8 100644 --- a/test/Ocelot.Provider.Rafty.IntegrationTests/Ocelot.Provider.Rafty.IntegrationTests.csproj +++ b/test/Ocelot.Provider.Rafty.IntegrationTests/Ocelot.Provider.Rafty.IntegrationTests.csproj @@ -21,7 +21,7 @@ - + all diff --git a/test/Ocelot.Provider.Rafty.IntegrationTests/RaftTests.cs b/test/Ocelot.Provider.Rafty.IntegrationTests/RaftTests.cs new file mode 100644 index 0000000..180bcc5 --- /dev/null +++ b/test/Ocelot.Provider.Rafty.IntegrationTests/RaftTests.cs @@ -0,0 +1,510 @@ +namespace Ocelot.Provider.Rafty.IntegrationTests +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Net.Http; + using System.Net.Http.Headers; + using System.Threading; + using System.Threading.Tasks; + using Configuration.File; + using DependencyInjection; + using global::Rafty.Infrastructure; + using Microsoft.AspNetCore.Hosting; + using Microsoft.Data.Sqlite; + using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.DependencyInjection; + using Middleware; + using Newtonsoft.Json; + using Shouldly; + using Xunit; + using Xunit.Abstractions; + + public class RaftTests : IDisposable + { + private readonly List _builders; + private readonly List _webHostBuilders; + private readonly List _threads; + private FilePeers _peers; + private HttpClient _httpClient; + private readonly HttpClient _httpClientForAssertions; + private BearerToken _token; + private HttpResponseMessage _response; + private static readonly object _lock = new object(); + private ITestOutputHelper _output; + + public RaftTests(ITestOutputHelper output) + { + _output = output; + _httpClientForAssertions = new HttpClient(); + _webHostBuilders = new List(); + _builders = new List(); + _threads = new List(); + } + + [Fact(Skip = "Still not stable, more work required in rafty..")] + public async Task should_persist_command_to_five_servers() + { + var peers = new List + { + new FilePeer {HostAndPort = "http://localhost:5000"}, + + new FilePeer {HostAndPort = "http://localhost:5001"}, + + new FilePeer {HostAndPort = "http://localhost:5002"}, + + new FilePeer {HostAndPort = "http://localhost:5003"}, + + new FilePeer {HostAndPort = "http://localhost:5004"} + }; + + var configuration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + } + }; + + var updatedConfiguration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + }, + ReRoutes = new List() + { + new FileReRoute() + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "127.0.0.1", + Port = 80, + } + }, + DownstreamScheme = "http", + DownstreamPathTemplate = "/geoffrey", + UpstreamHttpMethod = new List { "get" }, + UpstreamPathTemplate = "/" + }, + new FileReRoute() + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "123.123.123", + Port = 443, + } + }, + DownstreamScheme = "https", + DownstreamPathTemplate = "/blooper/{productId}", + UpstreamHttpMethod = new List { "post" }, + UpstreamPathTemplate = "/test" + } + } + }; + + var command = new UpdateFileConfiguration(updatedConfiguration); + GivenThePeersAre(peers); + GivenThereIsAConfiguration(configuration); + GivenFiveServersAreRunning(); + await GivenIHaveAnOcelotToken("/administration"); + await WhenISendACommandIntoTheCluster(command); + Thread.Sleep(5000); + await ThenTheCommandIsReplicatedToAllStateMachines(command); + } + + [Fact(Skip = "Still not stable, more work required in rafty..")] + public async Task should_persist_command_to_five_servers_when_using_administration_api() + { + var peers = new List + { + new FilePeer {HostAndPort = "http://localhost:5005"}, + + new FilePeer {HostAndPort = "http://localhost:5006"}, + + new FilePeer {HostAndPort = "http://localhost:5007"}, + + new FilePeer {HostAndPort = "http://localhost:5008"}, + + new FilePeer {HostAndPort = "http://localhost:5009"} + }; + + var configuration = new FileConfiguration + { + }; + + var updatedConfiguration = new FileConfiguration + { + ReRoutes = new List() + { + new FileReRoute() + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "127.0.0.1", + Port = 80, + } + }, + DownstreamScheme = "http", + DownstreamPathTemplate = "/geoffrey", + UpstreamHttpMethod = new List { "get" }, + UpstreamPathTemplate = "/" + }, + new FileReRoute() + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "123.123.123", + Port = 443, + } + }, + DownstreamScheme = "https", + DownstreamPathTemplate = "/blooper/{productId}", + UpstreamHttpMethod = new List { "post" }, + UpstreamPathTemplate = "/test" + } + } + }; + + var command = new UpdateFileConfiguration(updatedConfiguration); + GivenThePeersAre(peers); + GivenThereIsAConfiguration(configuration); + GivenFiveServersAreRunning(); + await GivenIHaveAnOcelotToken("/administration"); + GivenIHaveAddedATokenToMyRequest(); + await WhenIPostOnTheApiGateway("/administration/configuration", updatedConfiguration); + await ThenTheCommandIsReplicatedToAllStateMachines(command); + } + + private void GivenThePeersAre(List peers) + { + FilePeers filePeers = new FilePeers(); + filePeers.Peers.AddRange(peers); + var json = JsonConvert.SerializeObject(filePeers); + File.WriteAllText("peers.json", json); + _httpClient = new HttpClient(); + var ocelotBaseUrl = peers[0].HostAndPort; + _httpClient.BaseAddress = new Uri(ocelotBaseUrl); + } + + private async Task WhenISendACommandIntoTheCluster(UpdateFileConfiguration command) + { + async Task SendCommand() + { + try + { + var p = _peers.Peers.First(); + var json = JsonConvert.SerializeObject(command, new JsonSerializerSettings() + { + TypeNameHandling = TypeNameHandling.All + }); + var httpContent = new StringContent(json); + httpContent.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + using (var httpClient = new HttpClient()) + { + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); + var response = await httpClient.PostAsync($"{p.HostAndPort}/administration/raft/command", httpContent); + response.EnsureSuccessStatusCode(); + var content = await response.Content.ReadAsStringAsync(); + + var errorResult = JsonConvert.DeserializeObject>(content); + + if (!string.IsNullOrEmpty(errorResult.Error)) + { + return false; + } + + var okResult = JsonConvert.DeserializeObject>(content); + + if (okResult.Command.Configuration.ReRoutes.Count == 2) + { + return true; + } + } + + return false; + } + catch (Exception e) + { + Console.WriteLine(e); + return false; + } + } + + var commandSent = await Wait.WaitFor(40000).Until(async () => + { + var result = await SendCommand(); + Thread.Sleep(1000); + return result; + }); + + commandSent.ShouldBeTrue(); + } + + private async Task ThenTheCommandIsReplicatedToAllStateMachines(UpdateFileConfiguration expecteds) + { + async Task CommandCalledOnAllStateMachines() + { + try + { + var passed = 0; + foreach (var peer in _peers.Peers) + { + var path = $"{peer.HostAndPort.Replace("/", "").Replace(":", "")}.db"; + using (var connection = new SqliteConnection($"Data Source={path};")) + { + connection.Open(); + var sql = @"select count(id) from logs"; + using (var command = new SqliteCommand(sql, connection)) + { + var index = Convert.ToInt32(command.ExecuteScalar()); + index.ShouldBe(1); + } + } + + _httpClientForAssertions.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); + var result = await _httpClientForAssertions.GetAsync($"{peer.HostAndPort}/administration/configuration"); + var json = await result.Content.ReadAsStringAsync(); + var response = JsonConvert.DeserializeObject(json, new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All }); + response.GlobalConfiguration.RequestIdKey.ShouldBe(expecteds.Configuration.GlobalConfiguration.RequestIdKey); + response.GlobalConfiguration.ServiceDiscoveryProvider.Host.ShouldBe(expecteds.Configuration.GlobalConfiguration.ServiceDiscoveryProvider.Host); + response.GlobalConfiguration.ServiceDiscoveryProvider.Port.ShouldBe(expecteds.Configuration.GlobalConfiguration.ServiceDiscoveryProvider.Port); + + for (var i = 0; i < response.ReRoutes.Count; i++) + { + for (var j = 0; j < response.ReRoutes[i].DownstreamHostAndPorts.Count; j++) + { + var res = response.ReRoutes[i].DownstreamHostAndPorts[j]; + var expected = expecteds.Configuration.ReRoutes[i].DownstreamHostAndPorts[j]; + res.Host.ShouldBe(expected.Host); + res.Port.ShouldBe(expected.Port); + } + + response.ReRoutes[i].DownstreamPathTemplate.ShouldBe(expecteds.Configuration.ReRoutes[i].DownstreamPathTemplate); + response.ReRoutes[i].DownstreamScheme.ShouldBe(expecteds.Configuration.ReRoutes[i].DownstreamScheme); + response.ReRoutes[i].UpstreamPathTemplate.ShouldBe(expecteds.Configuration.ReRoutes[i].UpstreamPathTemplate); + response.ReRoutes[i].UpstreamHttpMethod.ShouldBe(expecteds.Configuration.ReRoutes[i].UpstreamHttpMethod); + } + + passed++; + } + + return passed == 5; + } + catch (Exception e) + { + //_output.WriteLine($"{e.Message}, {e.StackTrace}"); + Console.WriteLine(e); + return false; + } + } + + var commandOnAllStateMachines = await Wait.WaitFor(40000).Until(async () => + { + var result = await CommandCalledOnAllStateMachines(); + Thread.Sleep(1000); + return result; + }); + + commandOnAllStateMachines.ShouldBeTrue(); + } + + private async Task WhenIPostOnTheApiGateway(string url, FileConfiguration updatedConfiguration) + { + async Task SendCommand() + { + var json = JsonConvert.SerializeObject(updatedConfiguration); + + var content = new StringContent(json); + + content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + + _response = await _httpClient.PostAsync(url, content); + + var responseContent = await _response.Content.ReadAsStringAsync(); + + if (responseContent == "There was a problem. This error message sucks raise an issue in GitHub.") + { + return false; + } + + if (string.IsNullOrEmpty(responseContent)) + { + return false; + } + + return _response.IsSuccessStatusCode; + } + + var commandSent = await Wait.WaitFor(40000).Until(async () => + { + var result = await SendCommand(); + Thread.Sleep(1000); + return result; + }); + + commandSent.ShouldBeTrue(); + } + + private void GivenIHaveAddedATokenToMyRequest() + { + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); + } + + private async Task GivenIHaveAnOcelotToken(string adminPath) + { + async Task AddToken() + { + try + { + var tokenUrl = $"{adminPath}/connect/token"; + var formData = new List> + { + new KeyValuePair("client_id", "admin"), + new KeyValuePair("client_secret", "secret"), + new KeyValuePair("scope", "admin"), + new KeyValuePair("grant_type", "client_credentials") + }; + var content = new FormUrlEncodedContent(formData); + + var response = await _httpClient.PostAsync(tokenUrl, content); + var responseContent = await response.Content.ReadAsStringAsync(); + if (!response.IsSuccessStatusCode) + { + return false; + } + + _token = JsonConvert.DeserializeObject(responseContent); + var configPath = $"{adminPath}/.well-known/openid-configuration"; + response = await _httpClient.GetAsync(configPath); + return response.IsSuccessStatusCode; + } + catch (Exception) + { + return false; + } + } + + var addToken = await Wait.WaitFor(40000).Until(async () => + { + var result = await AddToken(); + Thread.Sleep(1000); + return result; + }); + + addToken.ShouldBeTrue(); + } + + private void GivenThereIsAConfiguration(FileConfiguration fileConfiguration) + { + var configurationPath = $"{Directory.GetCurrentDirectory()}/ocelot.json"; + + var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration); + + if (File.Exists(configurationPath)) + { + File.Delete(configurationPath); + } + + File.WriteAllText(configurationPath, jsonConfiguration); + + var text = File.ReadAllText(configurationPath); + + configurationPath = $"{AppContext.BaseDirectory}/ocelot.json"; + + if (File.Exists(configurationPath)) + { + File.Delete(configurationPath); + } + + File.WriteAllText(configurationPath, jsonConfiguration); + + text = File.ReadAllText(configurationPath); + } + + private void GivenAServerIsRunning(string url) + { + lock (_lock) + { + IWebHostBuilder webHostBuilder = new WebHostBuilder(); + webHostBuilder.UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); + config.AddJsonFile("ocelot.json", false, false); + config.AddJsonFile("peers.json", optional: true, reloadOnChange: false); +#pragma warning disable CS0618 + config.AddOcelotBaseUrl(url); +#pragma warning restore CS0618 + config.AddEnvironmentVariables(); + }) + .ConfigureServices(x => + { + x.AddSingleton(new NodeId(url)); + x + .AddOcelot() + .AddAdministration("/administration", "secret") + .AddRafty(); + }) + .Configure(app => + { + app.UseOcelot().Wait(); + }); + + var builder = webHostBuilder.Build(); + builder.Start(); + + _webHostBuilders.Add(webHostBuilder); + _builders.Add(builder); + } + } + + private void GivenFiveServersAreRunning() + { + var bytes = File.ReadAllText("peers.json"); + _peers = JsonConvert.DeserializeObject(bytes); + + foreach (var peer in _peers.Peers) + { + File.Delete(peer.HostAndPort.Replace("/", "").Replace(":", "")); + File.Delete($"{peer.HostAndPort.Replace("/", "").Replace(":", "")}.db"); + var thread = new Thread(() => GivenAServerIsRunning(peer.HostAndPort)); + thread.Start(); + _threads.Add(thread); + } + } + + public void Dispose() + { + foreach (var builder in _builders) + { + builder?.Dispose(); + } + + foreach (var peer in _peers.Peers) + { + try + { + File.Delete(peer.HostAndPort.Replace("/", "").Replace(":", "")); + File.Delete($"{peer.HostAndPort.Replace("/", "").Replace(":", "")}.db"); + } + catch (Exception e) + { + Console.WriteLine(e); + } + } + } + } +} diff --git a/test/Ocelot.Provider.Rafty.ManualTest/Ocelot.Provider.Rafty.ManualTest.csproj b/test/Ocelot.Provider.Rafty.ManualTest/Ocelot.Provider.Rafty.ManualTest.csproj index e4f6c9e..fdc2eca 100644 --- a/test/Ocelot.Provider.Rafty.ManualTest/Ocelot.Provider.Rafty.ManualTest.csproj +++ b/test/Ocelot.Provider.Rafty.ManualTest/Ocelot.Provider.Rafty.ManualTest.csproj @@ -34,8 +34,9 @@ - + + all diff --git a/test/Ocelot.Provider.Rafty.UnitTests/Ocelot.Provider.Rafty.UnitTests.csproj b/test/Ocelot.Provider.Rafty.UnitTests/Ocelot.Provider.Rafty.UnitTests.csproj index b7a68ec..690a975 100644 --- a/test/Ocelot.Provider.Rafty.UnitTests/Ocelot.Provider.Rafty.UnitTests.csproj +++ b/test/Ocelot.Provider.Rafty.UnitTests/Ocelot.Provider.Rafty.UnitTests.csproj @@ -41,7 +41,7 @@ - + all diff --git a/test/Ocelot.Provider.Rafty.UnitTests/OcelotAdministrationBuilderExtensionsTests.cs b/test/Ocelot.Provider.Rafty.UnitTests/OcelotAdministrationBuilderExtensionsTests.cs new file mode 100644 index 0000000..6cfca61 --- /dev/null +++ b/test/Ocelot.Provider.Rafty.UnitTests/OcelotAdministrationBuilderExtensionsTests.cs @@ -0,0 +1,76 @@ +namespace Ocelot.Provider.Rafty.UnitTests.Properties +{ + using DependencyInjection; + using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.DependencyInjection; + using Shouldly; + using TestStack.BDDfy; + using Xunit; + using Microsoft.AspNetCore.Hosting; + using Microsoft.AspNetCore.Hosting.Internal; + using System; + using System.Collections.Generic; + + public class OcelotAdministrationBuilderExtensionsTests + { + private readonly IServiceCollection _services; + private IServiceProvider _serviceProvider; + private readonly IConfiguration _configRoot; + private IOcelotBuilder _ocelotBuilder; + private Exception _ex; + + public OcelotAdministrationBuilderExtensionsTests() + { + _configRoot = new ConfigurationRoot(new List()); + _services = new ServiceCollection(); + _services.AddSingleton(); + _services.AddSingleton(_configRoot); + } + + [Fact] + public void should_set_up_rafty() + { + this.Given(x => WhenISetUpOcelotServices()) + .When(x => WhenISetUpRafty()) + .Then(x => ThenAnExceptionIsntThrown()) + .Then(x => ThenTheCorrectAdminPathIsRegitered()) + .BDDfy(); + } + + private void WhenISetUpRafty() + { + try + { + _ocelotBuilder.AddAdministration("/administration", "secret").AddRafty(); + } + catch (Exception e) + { + _ex = e; + } + } + + private void WhenISetUpOcelotServices() + { + try + { + _ocelotBuilder = _services.AddOcelot(_configRoot); + } + catch (Exception e) + { + _ex = e; + } + } + + private void ThenAnExceptionIsntThrown() + { + _ex.ShouldBeNull(); + } + + private void ThenTheCorrectAdminPathIsRegitered() + { + _serviceProvider = _services.BuildServiceProvider(); + var path = _serviceProvider.GetService(); + path.Path.ShouldBe("/administration"); + } + } +} diff --git a/test/Ocelot.Provider.Rafty.UnitTests/OcelotFiniteStateMachineTests.cs b/test/Ocelot.Provider.Rafty.UnitTests/OcelotFiniteStateMachineTests.cs new file mode 100644 index 0000000..68cf17e --- /dev/null +++ b/test/Ocelot.Provider.Rafty.UnitTests/OcelotFiniteStateMachineTests.cs @@ -0,0 +1,44 @@ +namespace Ocelot.Provider.Rafty.UnitTests +{ + using Configuration.Setter; + using Moq; + using TestStack.BDDfy; + using Xunit; + + public class OcelotFiniteStateMachineTests + { + private UpdateFileConfiguration _command; + private readonly OcelotFiniteStateMachine _fsm; + private readonly Mock _setter; + + public OcelotFiniteStateMachineTests() + { + _setter = new Mock(); + _fsm = new OcelotFiniteStateMachine(_setter.Object); + } + + [Fact] + public void should_handle_update_file_configuration_command() + { + this.Given(x => GivenACommand(new UpdateFileConfiguration(new Ocelot.Configuration.File.FileConfiguration()))) + .When(x => WhenTheCommandIsHandled()) + .Then(x => ThenTheStateIsUpdated()) + .BDDfy(); + } + + private void GivenACommand(UpdateFileConfiguration command) + { + _command = command; + } + + private void WhenTheCommandIsHandled() + { + _fsm.Handle(new global::Rafty.Log.LogEntry(_command, _command.GetType(), 0)).Wait(); + } + + private void ThenTheStateIsUpdated() + { + _setter.Verify(x => x.Set(_command.Configuration), Times.Once); + } + } +} diff --git a/test/Ocelot.Provider.Rafty.UnitTests/RaftyFileConfigurationSetterTests.cs b/test/Ocelot.Provider.Rafty.UnitTests/RaftyFileConfigurationSetterTests.cs new file mode 100644 index 0000000..c014510 --- /dev/null +++ b/test/Ocelot.Provider.Rafty.UnitTests/RaftyFileConfigurationSetterTests.cs @@ -0,0 +1,51 @@ +namespace Ocelot.Provider.Rafty.UnitTests +{ + using System.Threading.Tasks; + using Configuration.File; + using global::Rafty.Concensus.Node; + using global::Rafty.Infrastructure; + using Moq; + using Shouldly; + using Xunit; + + public class RaftyFileConfigurationSetterTests + { + private readonly RaftyFileConfigurationSetter _setter; + private readonly Mock _node; + + public RaftyFileConfigurationSetterTests() + { + _node = new Mock(); + _setter = new RaftyFileConfigurationSetter(_node.Object); + } + + [Fact] + public async Task should_return_ok() + { + var fileConfig = new FileConfiguration(); + + var response = new OkResponse(new UpdateFileConfiguration(fileConfig)); + + _node.Setup(x => x.Accept(It.IsAny())) + .ReturnsAsync(response); + + var result = await _setter.Set(fileConfig); + result.IsError.ShouldBeFalse(); + } + + [Fact] + public async Task should_return_not_ok() + { + var fileConfig = new FileConfiguration(); + + var response = new ErrorResponse("error", new UpdateFileConfiguration(fileConfig)); + + _node.Setup(x => x.Accept(It.IsAny())) + .ReturnsAsync(response); + + var result = await _setter.Set(fileConfig); + + result.IsError.ShouldBeTrue(); + } + } +} From bbdd4d2f8bb00a7bc464ad075d0a14c48319f17a Mon Sep 17 00:00:00 2001 From: Tom Pallister Date: Sun, 12 Aug 2018 21:02:06 +0100 Subject: [PATCH 2/3] updated to Ocelot 10.0.1 --- README.md | 7 +++---- src/Ocelot.Provider.Rafty/Ocelot.Provider.Rafty.csproj | 4 ++-- .../OcelotAdministrationBuilderExtensions.cs | 2 ++ .../Ocelot.Provider.Rafty.AcceptanceTests.csproj | 2 +- .../Ocelot.Provider.Rafty.Benchmarks.csproj | 2 +- .../Ocelot.Provider.Rafty.IntegrationTests.csproj | 2 +- .../Ocelot.Provider.Rafty.ManualTest.csproj | 2 +- test/Ocelot.Provider.Rafty.ManualTest/Program.cs | 3 ++- .../Ocelot.Provider.Rafty.UnitTests.csproj | 2 +- 9 files changed, 14 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 4ca668d..1f258aa 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,9 @@ [](http://threemammals.com/ocelot) -[![Build status](https://ci.appveyor.com/api/projects/status/8ry4ailt7rr5mbu7?svg=true)](https://ci.appveyor.com/project/TomPallister/ocelot-cache-cachemanager) -Windows (AppVeyor) -[![Build Status](https://travis-ci.org/ThreeMammals/Ocelot.Provider.Rafty.svg?branch=master)](https://travis-ci.org/ThreeMammals/Ocelot.Provider.Rafty) Linux & OSX (Travis) +[![Build status](https://ci.appveyor.com/api/projects/status/up4ro24ua58h7v87?svg=true)](https://ci.appveyor.com/project/TomPallister/ocelot-provider-rafty) Windows (AppVeyor) +[![Build Status](https://travis-ci.org/ThreeMammals/Ocelot.Provider.Rafty.svg?branch=develop)](https://travis-ci.org/ThreeMammals/Ocelot.Provider.Rafty) Linux & OSX (Travis) -[![Coverage Status](https://coveralls.io/repos/github/ThreeMammals/Ocelot.Provider.Rafty/badge.svg?branch=develop)](https://coveralls.io/github/ThreeMammals/Ocelot.Provider.Rafty?branch=develop) +[![Coverage Status](https://coveralls.io/repos/github/ThreeMammals/Ocelot.Provider.Rafty/badge.svg)](https://coveralls.io/github/ThreeMammals/Ocelot.Provider.Rafty) # Ocelot diff --git a/src/Ocelot.Provider.Rafty/Ocelot.Provider.Rafty.csproj b/src/Ocelot.Provider.Rafty/Ocelot.Provider.Rafty.csproj index ca85214..f82a18e 100644 --- a/src/Ocelot.Provider.Rafty/Ocelot.Provider.Rafty.csproj +++ b/src/Ocelot.Provider.Rafty/Ocelot.Provider.Rafty.csproj @@ -4,7 +4,7 @@ 2.0.0 2.0.0 true - Provides Ocelot extensions to use CacheManager.Net + Provides Ocelot extensions to use Rafty Ocelot.Provider.Rafty 0.0.0-dev Ocelot.Provider.Rafty @@ -26,7 +26,7 @@ True - + all diff --git a/src/Ocelot.Provider.Rafty/OcelotAdministrationBuilderExtensions.cs b/src/Ocelot.Provider.Rafty/OcelotAdministrationBuilderExtensions.cs index 8a94adc..a048765 100644 --- a/src/Ocelot.Provider.Rafty/OcelotAdministrationBuilderExtensions.cs +++ b/src/Ocelot.Provider.Rafty/OcelotAdministrationBuilderExtensions.cs @@ -6,6 +6,8 @@ using global::Rafty.FiniteStateMachine; using global::Rafty.Infrastructure; using global::Rafty.Log; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.DependencyInjection.Extensions; public static class OcelotAdministrationBuilderExtensions { diff --git a/test/Ocelot.Provider.Rafty.AcceptanceTests/Ocelot.Provider.Rafty.AcceptanceTests.csproj b/test/Ocelot.Provider.Rafty.AcceptanceTests/Ocelot.Provider.Rafty.AcceptanceTests.csproj index a8c4d16..f71de7e 100644 --- a/test/Ocelot.Provider.Rafty.AcceptanceTests/Ocelot.Provider.Rafty.AcceptanceTests.csproj +++ b/test/Ocelot.Provider.Rafty.AcceptanceTests/Ocelot.Provider.Rafty.AcceptanceTests.csproj @@ -33,7 +33,7 @@ - + all diff --git a/test/Ocelot.Provider.Rafty.Benchmarks/Ocelot.Provider.Rafty.Benchmarks.csproj b/test/Ocelot.Provider.Rafty.Benchmarks/Ocelot.Provider.Rafty.Benchmarks.csproj index cecc29a..eebfc52 100644 --- a/test/Ocelot.Provider.Rafty.Benchmarks/Ocelot.Provider.Rafty.Benchmarks.csproj +++ b/test/Ocelot.Provider.Rafty.Benchmarks/Ocelot.Provider.Rafty.Benchmarks.csproj @@ -15,7 +15,7 @@ - + all diff --git a/test/Ocelot.Provider.Rafty.IntegrationTests/Ocelot.Provider.Rafty.IntegrationTests.csproj b/test/Ocelot.Provider.Rafty.IntegrationTests/Ocelot.Provider.Rafty.IntegrationTests.csproj index bc5d5f8..2e12aa0 100644 --- a/test/Ocelot.Provider.Rafty.IntegrationTests/Ocelot.Provider.Rafty.IntegrationTests.csproj +++ b/test/Ocelot.Provider.Rafty.IntegrationTests/Ocelot.Provider.Rafty.IntegrationTests.csproj @@ -21,7 +21,7 @@ - + all diff --git a/test/Ocelot.Provider.Rafty.ManualTest/Ocelot.Provider.Rafty.ManualTest.csproj b/test/Ocelot.Provider.Rafty.ManualTest/Ocelot.Provider.Rafty.ManualTest.csproj index fdc2eca..4b161a1 100644 --- a/test/Ocelot.Provider.Rafty.ManualTest/Ocelot.Provider.Rafty.ManualTest.csproj +++ b/test/Ocelot.Provider.Rafty.ManualTest/Ocelot.Provider.Rafty.ManualTest.csproj @@ -34,7 +34,7 @@ - + diff --git a/test/Ocelot.Provider.Rafty.ManualTest/Program.cs b/test/Ocelot.Provider.Rafty.ManualTest/Program.cs index d964eb6..b277f99 100644 --- a/test/Ocelot.Provider.Rafty.ManualTest/Program.cs +++ b/test/Ocelot.Provider.Rafty.ManualTest/Program.cs @@ -25,7 +25,8 @@ public static void Main(string[] args) }) .ConfigureServices(s => { s.AddOcelot() - .AddSomething(); + .AddAdministration("/administration", "secret") + .AddRafty(); }) .ConfigureLogging((hostingContext, logging) => { diff --git a/test/Ocelot.Provider.Rafty.UnitTests/Ocelot.Provider.Rafty.UnitTests.csproj b/test/Ocelot.Provider.Rafty.UnitTests/Ocelot.Provider.Rafty.UnitTests.csproj index 690a975..f1a9356 100644 --- a/test/Ocelot.Provider.Rafty.UnitTests/Ocelot.Provider.Rafty.UnitTests.csproj +++ b/test/Ocelot.Provider.Rafty.UnitTests/Ocelot.Provider.Rafty.UnitTests.csproj @@ -41,7 +41,7 @@ - + all From c3240c32f683ba88d3452946121905a5a2e2cb59 Mon Sep 17 00:00:00 2001 From: Tom Pallister Date: Sun, 12 Aug 2018 21:11:25 +0100 Subject: [PATCH 3/3] force this for build --- tools/packages.config | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 tools/packages.config diff --git a/tools/packages.config b/tools/packages.config new file mode 100644 index 0000000..e52a2c7 --- /dev/null +++ b/tools/packages.config @@ -0,0 +1,4 @@ + + + +