mirror of
				https://github.com/nsnail/Ocelot.git
				synced 2025-11-04 12:10:50 +08:00 
			
		
		
		
	Feature/fix unstable int tests (#376)
* updated packages but build wont work * #245 implementing more stable rafty * #245 OK so these raft integration tests are passing everytime on my local mac now...lets see about the build servergit log * #245 added donation button * #245 removed file we dont need
This commit is contained in:
		@@ -83,6 +83,12 @@ advice on the easiest way to do things :)
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
Finally we mark all existing issues as help wanted, small, medium and large effort. If you want to contriute for the first time I suggest looking at a help wanted & small effort issue :)
 | 
					Finally we mark all existing issues as help wanted, small, medium and large effort. If you want to contriute for the first time I suggest looking at a help wanted & small effort issue :)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Donate
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If you think this project is worth supporting financially please make a contribution using the button below!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[](https://www.paypal.me/ThreeMammals/)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Things that are currently annoying me
 | 
					## Things that are currently annoying me
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[ Get more details at **codescene.io**.](https://codescene.io/projects/697/jobs/latest-successful/results)
 | 
					[ Get more details at **codescene.io**.](https://codescene.io/projects/697/jobs/latest-successful/results)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,10 +5,10 @@ using Microsoft.AspNetCore.Mvc;
 | 
				
			|||||||
using Ocelot.Configuration.File;
 | 
					using Ocelot.Configuration.File;
 | 
				
			||||||
using Ocelot.Configuration.Setter;
 | 
					using Ocelot.Configuration.Setter;
 | 
				
			||||||
using Ocelot.Raft;
 | 
					using Ocelot.Raft;
 | 
				
			||||||
using Rafty.Concensus;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Ocelot.Configuration
 | 
					namespace Ocelot.Configuration
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
 | 
					    using Rafty.Concensus.Node;
 | 
				
			||||||
    using Repository;
 | 
					    using Repository;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    [Authorize]
 | 
					    [Authorize]
 | 
				
			||||||
@@ -50,7 +50,7 @@ namespace Ocelot.Configuration
 | 
				
			|||||||
                {
 | 
					                {
 | 
				
			||||||
                    var node = (INode)test;
 | 
					                    var node = (INode)test;
 | 
				
			||||||
                    var result = await node.Accept(new UpdateFileConfiguration(fileConfiguration));
 | 
					                    var result = await node.Accept(new UpdateFileConfiguration(fileConfiguration));
 | 
				
			||||||
                    if (result.GetType() == typeof(Rafty.Concensus.ErrorResponse<UpdateFileConfiguration>))
 | 
					                    if (result.GetType() == typeof(Rafty.Infrastructure.ErrorResponse<UpdateFileConfiguration>))
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        return new BadRequestObjectResult("There was a problem. This error message sucks raise an issue in GitHub.");
 | 
					                        return new BadRequestObjectResult("There was a problem. This error message sucks raise an issue in GitHub.");
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,6 +8,8 @@ using Rafty.Log;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
namespace Ocelot.DependencyInjection
 | 
					namespace Ocelot.DependencyInjection
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
 | 
					    using Rafty.Concensus.Node;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public class OcelotAdministrationBuilder : IOcelotAdministrationBuilder
 | 
					    public class OcelotAdministrationBuilder : IOcelotAdministrationBuilder
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        private readonly IServiceCollection _services;
 | 
					        private readonly IServiceCollection _services;
 | 
				
			||||||
@@ -21,7 +23,7 @@ namespace Ocelot.DependencyInjection
 | 
				
			|||||||
        
 | 
					        
 | 
				
			||||||
        public IOcelotAdministrationBuilder AddRafty()
 | 
					        public IOcelotAdministrationBuilder AddRafty()
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            var settings = new InMemorySettings(4000, 5000, 100, 5000);
 | 
					            var settings = new InMemorySettings(4000, 6000, 100, 10000);
 | 
				
			||||||
            _services.AddSingleton<ILog, SqlLiteLog>();
 | 
					            _services.AddSingleton<ILog, SqlLiteLog>();
 | 
				
			||||||
            _services.AddSingleton<IFiniteStateMachine, OcelotFiniteStateMachine>();
 | 
					            _services.AddSingleton<IFiniteStateMachine, OcelotFiniteStateMachine>();
 | 
				
			||||||
            _services.AddSingleton<ISettings>(settings);
 | 
					            _services.AddSingleton<ISettings>(settings);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,6 +18,7 @@
 | 
				
			|||||||
    using Rafty.Infrastructure;
 | 
					    using Rafty.Infrastructure;
 | 
				
			||||||
    using Ocelot.Middleware.Pipeline;
 | 
					    using Ocelot.Middleware.Pipeline;
 | 
				
			||||||
    using Pivotal.Discovery.Client;
 | 
					    using Pivotal.Discovery.Client;
 | 
				
			||||||
 | 
					    using Rafty.Concensus.Node;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public static class OcelotMiddlewareExtensions
 | 
					    public static class OcelotMiddlewareExtensions
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
@@ -91,7 +92,7 @@
 | 
				
			|||||||
            applicationLifetime.ApplicationStopping.Register(() => OnShutdown(builder));
 | 
					            applicationLifetime.ApplicationStopping.Register(() => OnShutdown(builder));
 | 
				
			||||||
            var node = (INode)builder.ApplicationServices.GetService(typeof(INode));
 | 
					            var node = (INode)builder.ApplicationServices.GetService(typeof(INode));
 | 
				
			||||||
            var nodeId = (NodeId)builder.ApplicationServices.GetService(typeof(NodeId));
 | 
					            var nodeId = (NodeId)builder.ApplicationServices.GetService(typeof(NodeId));
 | 
				
			||||||
            node.Start(nodeId.Id);
 | 
					            node.Start(nodeId);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private static async Task<IInternalConfiguration> CreateConfiguration(IApplicationBuilder builder)
 | 
					        private static async Task<IInternalConfiguration> CreateConfiguration(IApplicationBuilder builder)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -55,7 +55,7 @@
 | 
				
			|||||||
    <PackageReference Include="Consul" Version="0.7.2.4"/>
 | 
					    <PackageReference Include="Consul" Version="0.7.2.4"/>
 | 
				
			||||||
    <PackageReference Include="Polly" Version="6.0.1"/>
 | 
					    <PackageReference Include="Polly" Version="6.0.1"/>
 | 
				
			||||||
    <PackageReference Include="IdentityServer4" Version="2.2.0"/>
 | 
					    <PackageReference Include="IdentityServer4" Version="2.2.0"/>
 | 
				
			||||||
    <PackageReference Include="Rafty" Version="0.4.3" />
 | 
					 | 
				
			||||||
    <PackageReference Include="Pivotal.Discovery.ClientCore" Version="2.0.1"/>
 | 
					    <PackageReference Include="Pivotal.Discovery.ClientCore" Version="2.0.1"/>
 | 
				
			||||||
 | 
					    <PackageReference Include="Rafty" Version="0.4.4"/>
 | 
				
			||||||
  </ItemGroup>
 | 
					  </ItemGroup>
 | 
				
			||||||
</Project>
 | 
					</Project>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,36 +0,0 @@
 | 
				
			|||||||
using System;
 | 
					 | 
				
			||||||
using System.IO;
 | 
					 | 
				
			||||||
using System.Threading.Tasks;
 | 
					 | 
				
			||||||
using Newtonsoft.Json;
 | 
					 | 
				
			||||||
using Rafty.FiniteStateMachine;
 | 
					 | 
				
			||||||
using Rafty.Infrastructure;
 | 
					 | 
				
			||||||
using Rafty.Log;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace Ocelot.Raft
 | 
					 | 
				
			||||||
{    
 | 
					 | 
				
			||||||
    [ExcludeFromCoverage]
 | 
					 | 
				
			||||||
    public class FileFsm : IFiniteStateMachine
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        private string _id;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        public FileFsm(NodeId nodeId)
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            _id = nodeId.Id.Replace("/","").Replace(":","");
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        public Task Handle(LogEntry log)
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            try
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                var json = JsonConvert.SerializeObject(log.CommandData);
 | 
					 | 
				
			||||||
                File.AppendAllText(_id, json);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            catch(Exception exception)
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                Console.WriteLine(exception);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            return Task.CompletedTask;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -9,6 +9,8 @@ using Rafty.Infrastructure;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
namespace Ocelot.Raft
 | 
					namespace Ocelot.Raft
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
 | 
					    using Rafty.Concensus.Peers;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    [ExcludeFromCoverage]
 | 
					    [ExcludeFromCoverage]
 | 
				
			||||||
    public class FilePeersProvider : IPeersProvider
 | 
					    public class FilePeersProvider : IPeersProvider
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,6 +10,10 @@ using Rafty.FiniteStateMachine;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
namespace Ocelot.Raft
 | 
					namespace Ocelot.Raft
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
 | 
					    using Rafty.Concensus.Messages;
 | 
				
			||||||
 | 
					    using Rafty.Concensus.Peers;
 | 
				
			||||||
 | 
					    using Rafty.Infrastructure;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    [ExcludeFromCoverage]
 | 
					    [ExcludeFromCoverage]
 | 
				
			||||||
    public class HttpPeer : IPeer
 | 
					    public class HttpPeer : IPeer
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,7 +8,7 @@ namespace Ocelot.Raft
 | 
				
			|||||||
    [ExcludeFromCoverage]
 | 
					    [ExcludeFromCoverage]
 | 
				
			||||||
    public class OcelotFiniteStateMachine : IFiniteStateMachine
 | 
					    public class OcelotFiniteStateMachine : IFiniteStateMachine
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        private IFileConfigurationSetter _setter;
 | 
					        private readonly IFileConfigurationSetter _setter;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public OcelotFiniteStateMachine(IFileConfigurationSetter setter)
 | 
					        public OcelotFiniteStateMachine(IFileConfigurationSetter setter)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,6 +14,9 @@ using Rafty.FiniteStateMachine;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
namespace Ocelot.Raft
 | 
					namespace Ocelot.Raft
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
 | 
					    using Rafty.Concensus.Messages;
 | 
				
			||||||
 | 
					    using Rafty.Concensus.Node;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    [ExcludeFromCoverage]
 | 
					    [ExcludeFromCoverage]
 | 
				
			||||||
    [Authorize]
 | 
					    [Authorize]
 | 
				
			||||||
    [Route("raft")]
 | 
					    [Route("raft")]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,51 +1,62 @@
 | 
				
			|||||||
using System.IO;
 | 
					 | 
				
			||||||
using Rafty.Log;
 | 
					 | 
				
			||||||
using Microsoft.Data.Sqlite;
 | 
					 | 
				
			||||||
using Newtonsoft.Json;
 | 
					 | 
				
			||||||
using System;
 | 
					 | 
				
			||||||
using Rafty.Infrastructure;
 | 
					 | 
				
			||||||
using System.Collections.Generic;
 | 
					 | 
				
			||||||
using System.Threading.Tasks;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace Ocelot.Raft
 | 
					namespace Ocelot.Raft
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    //todo - use async await
 | 
					    using System;
 | 
				
			||||||
    [ExcludeFromCoverage]
 | 
					    using System.Collections.Generic;
 | 
				
			||||||
 | 
					    using System.IO;
 | 
				
			||||||
 | 
					    using System.Threading;
 | 
				
			||||||
 | 
					    using System.Threading.Tasks;
 | 
				
			||||||
 | 
					    using Microsoft.Data.Sqlite;
 | 
				
			||||||
 | 
					    using Microsoft.Extensions.Logging;
 | 
				
			||||||
 | 
					    using Newtonsoft.Json;
 | 
				
			||||||
 | 
					    using Rafty.Infrastructure;
 | 
				
			||||||
 | 
					    using Rafty.Log;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public class SqlLiteLog : ILog
 | 
					    public class SqlLiteLog : ILog
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        private string _path;
 | 
					        private readonly string _path;
 | 
				
			||||||
        private readonly object _lock = new object();
 | 
					        private readonly SemaphoreSlim _sempaphore = new SemaphoreSlim(1, 1);
 | 
				
			||||||
 | 
					        private readonly ILogger _logger;
 | 
				
			||||||
 | 
					        private readonly NodeId _nodeId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public SqlLiteLog(NodeId nodeId)
 | 
					        public SqlLiteLog(NodeId nodeId, ILoggerFactory loggerFactory)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
 | 
					            _logger = loggerFactory.CreateLogger<SqlLiteLog>();
 | 
				
			||||||
 | 
					            _nodeId = nodeId;
 | 
				
			||||||
            _path = $"{nodeId.Id.Replace("/", "").Replace(":", "")}.db";
 | 
					            _path = $"{nodeId.Id.Replace("/", "").Replace(":", "")}.db";
 | 
				
			||||||
 | 
					            _sempaphore.Wait();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (!File.Exists(_path))
 | 
					            if (!File.Exists(_path))
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                lock(_lock)
 | 
					                var fs = File.Create(_path);
 | 
				
			||||||
                {
 | 
					
 | 
				
			||||||
                    FileStream fs = File.Create(_path);
 | 
					 | 
				
			||||||
                fs.Dispose();
 | 
					                fs.Dispose();
 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                using (var connection = new SqliteConnection($"Data Source={_path};"))
 | 
					                using (var connection = new SqliteConnection($"Data Source={_path};"))
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    connection.Open();
 | 
					                    connection.Open();
 | 
				
			||||||
                    var sql = @"create table logs (
 | 
					
 | 
				
			||||||
 | 
					                    const string sql = @"create table logs (
 | 
				
			||||||
                        id integer primary key,
 | 
					                        id integer primary key,
 | 
				
			||||||
                        data text not null
 | 
					                        data text not null
 | 
				
			||||||
                    )";
 | 
					                    )";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    using (var command = new SqliteCommand(sql, connection))
 | 
					                    using (var command = new SqliteCommand(sql, connection))
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        var result = command.ExecuteNonQuery();
 | 
					                        var result = command.ExecuteNonQuery();
 | 
				
			||||||
                    }
 | 
					
 | 
				
			||||||
 | 
					                        _logger.LogInformation(result == 0
 | 
				
			||||||
 | 
					                            ? $"id: {_nodeId.Id} create database, result: {result}"
 | 
				
			||||||
 | 
					                            : $"id: {_nodeId.Id} did not create database., result: {result}");
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public Task<int> LastLogIndex()
 | 
					            _sempaphore.Release();
 | 
				
			||||||
        {
 | 
					        }
 | 
				
			||||||
            lock(_lock)
 | 
					
 | 
				
			||||||
 | 
					        public async Task<int> LastLogIndex()
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
 | 
					            _sempaphore.Wait();
 | 
				
			||||||
            var result = 1;
 | 
					            var result = 1;
 | 
				
			||||||
            using (var connection = new SqliteConnection($"Data Source={_path};"))
 | 
					            using (var connection = new SqliteConnection($"Data Source={_path};"))
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
@@ -53,7 +64,7 @@ namespace Ocelot.Raft
 | 
				
			|||||||
                var sql = @"select id from logs order by id desc limit 1";
 | 
					                var sql = @"select id from logs order by id desc limit 1";
 | 
				
			||||||
                using (var command = new SqliteCommand(sql, connection))
 | 
					                using (var command = new SqliteCommand(sql, connection))
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                        var index = Convert.ToInt32(command.ExecuteScalar());
 | 
					                    var index = Convert.ToInt32(await command.ExecuteScalarAsync());
 | 
				
			||||||
                    if (index > result)
 | 
					                    if (index > result)
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        result = index;
 | 
					                        result = index;
 | 
				
			||||||
@@ -61,14 +72,13 @@ namespace Ocelot.Raft
 | 
				
			|||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                return Task.FromResult(result);
 | 
					            _sempaphore.Release();
 | 
				
			||||||
            }
 | 
					            return result;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public Task<long> LastLogTerm ()
 | 
					        public async Task<long> LastLogTerm()
 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            lock(_lock)
 | 
					 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
 | 
					            _sempaphore.Wait();
 | 
				
			||||||
            long result = 0;
 | 
					            long result = 0;
 | 
				
			||||||
            using (var connection = new SqliteConnection($"Data Source={_path};"))
 | 
					            using (var connection = new SqliteConnection($"Data Source={_path};"))
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
@@ -76,8 +86,9 @@ namespace Ocelot.Raft
 | 
				
			|||||||
                var sql = @"select data from logs order by id desc limit 1";
 | 
					                var sql = @"select data from logs order by id desc limit 1";
 | 
				
			||||||
                using (var command = new SqliteCommand(sql, connection))
 | 
					                using (var command = new SqliteCommand(sql, connection))
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                        var data = Convert.ToString(command.ExecuteScalar());
 | 
					                    var data = Convert.ToString(await command.ExecuteScalarAsync());
 | 
				
			||||||
                        var jsonSerializerSettings = new JsonSerializerSettings() { 
 | 
					                    var jsonSerializerSettings = new JsonSerializerSettings()
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
                        TypeNameHandling = TypeNameHandling.All
 | 
					                        TypeNameHandling = TypeNameHandling.All
 | 
				
			||||||
                    };
 | 
					                    };
 | 
				
			||||||
                    var log = JsonConvert.DeserializeObject<LogEntry>(data, jsonSerializerSettings);
 | 
					                    var log = JsonConvert.DeserializeObject<LogEntry>(data, jsonSerializerSettings);
 | 
				
			||||||
@@ -87,15 +98,13 @@ namespace Ocelot.Raft
 | 
				
			|||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					            _sempaphore.Release();
 | 
				
			||||||
                return Task.FromResult(result);
 | 
					            return result;
 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public Task<int> Count ()
 | 
					        public async Task<int> Count()
 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            lock(_lock)
 | 
					 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
 | 
					            _sempaphore.Wait();
 | 
				
			||||||
            var result = 0;
 | 
					            var result = 0;
 | 
				
			||||||
            using (var connection = new SqliteConnection($"Data Source={_path};"))
 | 
					            using (var connection = new SqliteConnection($"Data Source={_path};"))
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
@@ -103,152 +112,183 @@ namespace Ocelot.Raft
 | 
				
			|||||||
                var sql = @"select count(id) from logs";
 | 
					                var sql = @"select count(id) from logs";
 | 
				
			||||||
                using (var command = new SqliteCommand(sql, connection))
 | 
					                using (var command = new SqliteCommand(sql, connection))
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                        var index = Convert.ToInt32(command.ExecuteScalar());
 | 
					                    var index = Convert.ToInt32(await command.ExecuteScalarAsync());
 | 
				
			||||||
                    if (index > result)
 | 
					                    if (index > result)
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        result = index;
 | 
					                        result = index;
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					            _sempaphore.Release();
 | 
				
			||||||
                return Task.FromResult(result);
 | 
					            return result;
 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public Task<int> Apply(LogEntry log)
 | 
					        public async Task<int> Apply(LogEntry log)
 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            lock(_lock)
 | 
					 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
 | 
					            _sempaphore.Wait();
 | 
				
			||||||
            using (var connection = new SqliteConnection($"Data Source={_path};"))
 | 
					            using (var connection = new SqliteConnection($"Data Source={_path};"))
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                connection.Open();
 | 
					                connection.Open();
 | 
				
			||||||
                    var jsonSerializerSettings = new JsonSerializerSettings() { 
 | 
					                var jsonSerializerSettings = new JsonSerializerSettings()
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
                    TypeNameHandling = TypeNameHandling.All
 | 
					                    TypeNameHandling = TypeNameHandling.All
 | 
				
			||||||
                };
 | 
					                };
 | 
				
			||||||
                var data = JsonConvert.SerializeObject(log, jsonSerializerSettings);
 | 
					                var data = JsonConvert.SerializeObject(log, jsonSerializerSettings);
 | 
				
			||||||
                    
 | 
					 | 
				
			||||||
                //todo - sql injection dont copy this..
 | 
					                //todo - sql injection dont copy this..
 | 
				
			||||||
                var sql = $"insert into logs (data) values ('{data}')";
 | 
					                var sql = $"insert into logs (data) values ('{data}')";
 | 
				
			||||||
 | 
					                _logger.LogInformation($"id: {_nodeId.Id}, sql: {sql}");
 | 
				
			||||||
                using (var command = new SqliteCommand(sql, connection))
 | 
					                using (var command = new SqliteCommand(sql, connection))
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                        var result = command.ExecuteNonQuery();
 | 
					                    var result = await command.ExecuteNonQueryAsync();
 | 
				
			||||||
 | 
					                    _logger.LogInformation($"id: {_nodeId.Id}, insert log result: {result}");
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                sql = "select last_insert_rowid()";
 | 
					                sql = "select last_insert_rowid()";
 | 
				
			||||||
                using (var command = new SqliteCommand(sql, connection))
 | 
					                using (var command = new SqliteCommand(sql, connection))
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                        var result = command.ExecuteScalar();
 | 
					                    var result = await command.ExecuteScalarAsync();
 | 
				
			||||||
                        return Task.FromResult(Convert.ToInt32(result));
 | 
					                    _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 Task DeleteConflictsFromThisLog(int index, LogEntry logEntry)
 | 
					        public async Task DeleteConflictsFromThisLog(int index, LogEntry logEntry)
 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            lock(_lock)
 | 
					 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
 | 
					            _sempaphore.Wait();
 | 
				
			||||||
            using (var connection = new SqliteConnection($"Data Source={_path};"))
 | 
					            using (var connection = new SqliteConnection($"Data Source={_path};"))
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                connection.Open();
 | 
					                connection.Open();
 | 
				
			||||||
                    
 | 
					 | 
				
			||||||
                //todo - sql injection dont copy this..
 | 
					                //todo - sql injection dont copy this..
 | 
				
			||||||
                var sql = $"select data from logs where id = {index};";
 | 
					                var sql = $"select data from logs where id = {index};";
 | 
				
			||||||
 | 
					                _logger.LogInformation($"id: {_nodeId.Id} sql: {sql}");
 | 
				
			||||||
                using (var command = new SqliteCommand(sql, connection))
 | 
					                using (var command = new SqliteCommand(sql, connection))
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                        var data = Convert.ToString(command.ExecuteScalar());
 | 
					                    var data = Convert.ToString(await command.ExecuteScalarAsync());
 | 
				
			||||||
                        var jsonSerializerSettings = new JsonSerializerSettings() { 
 | 
					                    var jsonSerializerSettings = new JsonSerializerSettings()
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
                        TypeNameHandling = TypeNameHandling.All
 | 
					                        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<LogEntry>(data, jsonSerializerSettings);
 | 
					                    var log = JsonConvert.DeserializeObject<LogEntry>(data, jsonSerializerSettings);
 | 
				
			||||||
                    if (logEntry != null && log != null && logEntry.Term != log.Term)
 | 
					                    if (logEntry != null && log != null && logEntry.Term != log.Term)
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        //todo - sql injection dont copy this..
 | 
					                        //todo - sql injection dont copy this..
 | 
				
			||||||
                        var deleteSql = $"delete from logs where id >= {index};";
 | 
					                        var deleteSql = $"delete from logs where id >= {index};";
 | 
				
			||||||
 | 
					                        _logger.LogInformation($"id: {_nodeId.Id} sql: {deleteSql}");
 | 
				
			||||||
                        using (var deleteCommand = new SqliteCommand(deleteSql, connection))
 | 
					                        using (var deleteCommand = new SqliteCommand(deleteSql, connection))
 | 
				
			||||||
                        {
 | 
					                        {
 | 
				
			||||||
                                var result = deleteCommand.ExecuteNonQuery();
 | 
					                            var result = await deleteCommand.ExecuteNonQueryAsync();
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					            _sempaphore.Release();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return Task.CompletedTask;
 | 
					        public async Task<bool> IsDuplicate(int index, LogEntry logEntry)
 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        public Task<LogEntry> Get(int index)
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            lock(_lock)
 | 
					 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
 | 
					            _sempaphore.Wait();
 | 
				
			||||||
            using (var connection = new SqliteConnection($"Data Source={_path};"))
 | 
					            using (var connection = new SqliteConnection($"Data Source={_path};"))
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                connection.Open();
 | 
					                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<LogEntry>(data, jsonSerializerSettings);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if (logEntry != null && log != null && logEntry.Term == log.Term)
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        _sempaphore.Release();
 | 
				
			||||||
 | 
					                        return true;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            _sempaphore.Release();
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public async Task<LogEntry> Get(int index)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            _sempaphore.Wait();
 | 
				
			||||||
 | 
					            using (var connection = new SqliteConnection($"Data Source={_path};"))
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                connection.Open();
 | 
				
			||||||
                //todo - sql injection dont copy this..
 | 
					                //todo - sql injection dont copy this..
 | 
				
			||||||
                var sql = $"select data from logs where id = {index}";
 | 
					                var sql = $"select data from logs where id = {index}";
 | 
				
			||||||
                using (var command = new SqliteCommand(sql, connection))
 | 
					                using (var command = new SqliteCommand(sql, connection))
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                        var data = Convert.ToString(command.ExecuteScalar());
 | 
					                    var data = Convert.ToString(await command.ExecuteScalarAsync());
 | 
				
			||||||
                        var jsonSerializerSettings = new JsonSerializerSettings() { 
 | 
					                    var jsonSerializerSettings = new JsonSerializerSettings()
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
                        TypeNameHandling = TypeNameHandling.All
 | 
					                        TypeNameHandling = TypeNameHandling.All
 | 
				
			||||||
                    };
 | 
					                    };
 | 
				
			||||||
                    var log = JsonConvert.DeserializeObject<LogEntry>(data, jsonSerializerSettings);
 | 
					                    var log = JsonConvert.DeserializeObject<LogEntry>(data, jsonSerializerSettings);
 | 
				
			||||||
                        return Task.FromResult(log);
 | 
					                    _sempaphore.Release();
 | 
				
			||||||
                    }
 | 
					                    return log;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public Task<List<(int index, LogEntry logEntry)>> GetFrom(int index)
 | 
					        public async Task<List<(int index, LogEntry logEntry)>> GetFrom(int index)
 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            lock(_lock)
 | 
					 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
 | 
					            _sempaphore.Wait();
 | 
				
			||||||
            var logsToReturn = new List<(int, LogEntry)>();
 | 
					            var logsToReturn = new List<(int, LogEntry)>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            using (var connection = new SqliteConnection($"Data Source={_path};"))
 | 
					            using (var connection = new SqliteConnection($"Data Source={_path};"))
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                connection.Open();
 | 
					                connection.Open();
 | 
				
			||||||
                    
 | 
					 | 
				
			||||||
                //todo - sql injection dont copy this..
 | 
					                //todo - sql injection dont copy this..
 | 
				
			||||||
                var sql = $"select id, data from logs where id >= {index}";
 | 
					                var sql = $"select id, data from logs where id >= {index}";
 | 
				
			||||||
                using (var command = new SqliteCommand(sql, connection))
 | 
					                using (var command = new SqliteCommand(sql, connection))
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                        using(var reader = command.ExecuteReader())
 | 
					                    using (var reader = await command.ExecuteReaderAsync())
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        while (reader.Read())
 | 
					                        while (reader.Read())
 | 
				
			||||||
                        {
 | 
					                        {
 | 
				
			||||||
                            var id = Convert.ToInt32(reader[0]);
 | 
					                            var id = Convert.ToInt32(reader[0]);
 | 
				
			||||||
                            var data = (string)reader[1];
 | 
					                            var data = (string)reader[1];
 | 
				
			||||||
                                var jsonSerializerSettings = new JsonSerializerSettings() { 
 | 
					                            var jsonSerializerSettings = new JsonSerializerSettings()
 | 
				
			||||||
 | 
					                            {
 | 
				
			||||||
                                TypeNameHandling = TypeNameHandling.All
 | 
					                                TypeNameHandling = TypeNameHandling.All
 | 
				
			||||||
                            };
 | 
					                            };
 | 
				
			||||||
                            var log = JsonConvert.DeserializeObject<LogEntry>(data, jsonSerializerSettings);
 | 
					                            var log = JsonConvert.DeserializeObject<LogEntry>(data, jsonSerializerSettings);
 | 
				
			||||||
                            logsToReturn.Add((id, log));
 | 
					                            logsToReturn.Add((id, log));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					                _sempaphore.Release();
 | 
				
			||||||
 | 
					                return logsToReturn;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                return Task.FromResult(logsToReturn);
 | 
					        public async Task<long> GetTermAtIndex(int index)
 | 
				
			||||||
            }        
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        public Task<long> GetTermAtIndex(int index)
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            lock(_lock)
 | 
					 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
 | 
					            _sempaphore.Wait();
 | 
				
			||||||
            long result = 0;
 | 
					            long result = 0;
 | 
				
			||||||
            using (var connection = new SqliteConnection($"Data Source={_path};"))
 | 
					            using (var connection = new SqliteConnection($"Data Source={_path};"))
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                connection.Open();
 | 
					                connection.Open();
 | 
				
			||||||
                    
 | 
					 | 
				
			||||||
                //todo - sql injection dont copy this..
 | 
					                //todo - sql injection dont copy this..
 | 
				
			||||||
                var sql = $"select data from logs where id = {index}";
 | 
					                var sql = $"select data from logs where id = {index}";
 | 
				
			||||||
                using (var command = new SqliteCommand(sql, connection))
 | 
					                using (var command = new SqliteCommand(sql, connection))
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                        var data = Convert.ToString(command.ExecuteScalar());
 | 
					                    var data = Convert.ToString(await command.ExecuteScalarAsync());
 | 
				
			||||||
                        var jsonSerializerSettings = new JsonSerializerSettings() { 
 | 
					                    var jsonSerializerSettings = new JsonSerializerSettings()
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
                        TypeNameHandling = TypeNameHandling.All
 | 
					                        TypeNameHandling = TypeNameHandling.All
 | 
				
			||||||
                    };
 | 
					                    };
 | 
				
			||||||
                    var log = JsonConvert.DeserializeObject<LogEntry>(data, jsonSerializerSettings);
 | 
					                    var log = JsonConvert.DeserializeObject<LogEntry>(data, jsonSerializerSettings);
 | 
				
			||||||
@@ -258,29 +298,24 @@ namespace Ocelot.Raft
 | 
				
			|||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					            _sempaphore.Release();
 | 
				
			||||||
                return Task.FromResult(result);
 | 
					            return result;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        }
 | 
					        public async Task Remove(int indexOfCommand)
 | 
				
			||||||
 | 
					 | 
				
			||||||
        public Task Remove(int indexOfCommand)
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            lock(_lock)
 | 
					 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
 | 
					            _sempaphore.Wait();
 | 
				
			||||||
            using (var connection = new SqliteConnection($"Data Source={_path};"))
 | 
					            using (var connection = new SqliteConnection($"Data Source={_path};"))
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                connection.Open();
 | 
					                connection.Open();
 | 
				
			||||||
                    
 | 
					 | 
				
			||||||
                //todo - sql injection dont copy this..
 | 
					                //todo - sql injection dont copy this..
 | 
				
			||||||
                var deleteSql = $"delete from logs where id >= {indexOfCommand};";
 | 
					                var deleteSql = $"delete from logs where id >= {indexOfCommand};";
 | 
				
			||||||
 | 
					                _logger.LogInformation($"id: {_nodeId.Id} Remove {deleteSql}");
 | 
				
			||||||
                using (var deleteCommand = new SqliteCommand(deleteSql, connection))
 | 
					                using (var deleteCommand = new SqliteCommand(deleteSql, connection))
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                        var result = deleteCommand.ExecuteNonQuery();
 | 
					                    var result = await deleteCommand.ExecuteNonQueryAsync();
 | 
				
			||||||
                    }
 | 
					                }
 | 
				
			||||||
                }
 | 
					            }
 | 
				
			||||||
            }
 | 
					            _sempaphore.Release();
 | 
				
			||||||
 | 
					 | 
				
			||||||
            return Task.CompletedTask;
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -55,6 +55,6 @@
 | 
				
			|||||||
    <PackageReference Include="Consul" Version="0.7.2.4" />
 | 
					    <PackageReference Include="Consul" Version="0.7.2.4" />
 | 
				
			||||||
    <PackageReference Include="xunit" Version="2.3.1" />
 | 
					    <PackageReference Include="xunit" Version="2.3.1" />
 | 
				
			||||||
    <PackageReference Include="Butterfly.Client.AspNetCore" Version="0.0.8" /> 
 | 
					    <PackageReference Include="Butterfly.Client.AspNetCore" Version="0.0.8" /> 
 | 
				
			||||||
    <PackageReference Include="Rafty" Version="0.4.3" />
 | 
					    <PackageReference Include="Rafty" Version="0.4.4" />
 | 
				
			||||||
  </ItemGroup>
 | 
					  </ItemGroup>
 | 
				
			||||||
</Project>
 | 
					</Project>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -44,7 +44,7 @@
 | 
				
			|||||||
    <PackageReference Include="Shouldly" Version="3.0.0" />
 | 
					    <PackageReference Include="Shouldly" Version="3.0.0" />
 | 
				
			||||||
    <PackageReference Include="TestStack.BDDfy" Version="4.3.2" />
 | 
					    <PackageReference Include="TestStack.BDDfy" Version="4.3.2" />
 | 
				
			||||||
    <PackageReference Include="Consul" Version="0.7.2.4" />
 | 
					    <PackageReference Include="Consul" Version="0.7.2.4" />
 | 
				
			||||||
    <PackageReference Include="Rafty" Version="0.4.3" />
 | 
					    <PackageReference Include="Rafty" Version="0.4.4" />
 | 
				
			||||||
    <PackageReference Include="Microsoft.Data.SQLite" Version="2.0.1" />
 | 
					    <PackageReference Include="Microsoft.Data.SQLite" Version="2.0.1" />
 | 
				
			||||||
  </ItemGroup>
 | 
					  </ItemGroup>
 | 
				
			||||||
</Project>
 | 
					</Project>
 | 
				
			||||||
@@ -23,6 +23,7 @@ using Ocelot.Middleware;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
namespace Ocelot.IntegrationTests
 | 
					namespace Ocelot.IntegrationTests
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
 | 
					    using System.Threading.Tasks;
 | 
				
			||||||
    using Xunit.Abstractions;
 | 
					    using Xunit.Abstractions;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public class RaftTests : IDisposable
 | 
					    public class RaftTests : IDisposable
 | 
				
			||||||
@@ -31,7 +32,7 @@ namespace Ocelot.IntegrationTests
 | 
				
			|||||||
        private readonly List<IWebHostBuilder> _webHostBuilders;
 | 
					        private readonly List<IWebHostBuilder> _webHostBuilders;
 | 
				
			||||||
        private readonly List<Thread> _threads;
 | 
					        private readonly List<Thread> _threads;
 | 
				
			||||||
        private FilePeers _peers;
 | 
					        private FilePeers _peers;
 | 
				
			||||||
        private readonly HttpClient _httpClient;
 | 
					        private HttpClient _httpClient;
 | 
				
			||||||
        private readonly HttpClient _httpClientForAssertions;
 | 
					        private readonly HttpClient _httpClientForAssertions;
 | 
				
			||||||
        private BearerToken _token;
 | 
					        private BearerToken _token;
 | 
				
			||||||
        private HttpResponseMessage _response;
 | 
					        private HttpResponseMessage _response;
 | 
				
			||||||
@@ -42,17 +43,27 @@ namespace Ocelot.IntegrationTests
 | 
				
			|||||||
        {
 | 
					        {
 | 
				
			||||||
            _output = output;
 | 
					            _output = output;
 | 
				
			||||||
            _httpClientForAssertions = new HttpClient();
 | 
					            _httpClientForAssertions = new HttpClient();
 | 
				
			||||||
            _httpClient = new HttpClient();
 | 
					 | 
				
			||||||
            var ocelotBaseUrl = "http://localhost:5000";
 | 
					 | 
				
			||||||
            _httpClient.BaseAddress = new Uri(ocelotBaseUrl);
 | 
					 | 
				
			||||||
            _webHostBuilders = new List<IWebHostBuilder>();
 | 
					            _webHostBuilders = new List<IWebHostBuilder>();
 | 
				
			||||||
            _builders = new List<IWebHost>();
 | 
					            _builders = new List<IWebHost>();
 | 
				
			||||||
            _threads = new List<Thread>();
 | 
					            _threads = new List<Thread>();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        [Fact(Skip = "still broken waiting for work in rafty")]
 | 
					        [Fact]
 | 
				
			||||||
        public void should_persist_command_to_five_servers()
 | 
					        public async Task should_persist_command_to_five_servers()
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
 | 
					            var peers = new List<FilePeer>
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                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
 | 
					            var configuration = new FileConfiguration
 | 
				
			||||||
             { 
 | 
					             { 
 | 
				
			||||||
                 GlobalConfiguration = new FileGlobalConfiguration
 | 
					                 GlobalConfiguration = new FileGlobalConfiguration
 | 
				
			||||||
@@ -101,17 +112,31 @@ namespace Ocelot.IntegrationTests
 | 
				
			|||||||
            };
 | 
					            };
 | 
				
			||||||
             
 | 
					             
 | 
				
			||||||
            var command = new UpdateFileConfiguration(updatedConfiguration);
 | 
					            var command = new UpdateFileConfiguration(updatedConfiguration);
 | 
				
			||||||
 | 
					            GivenThePeersAre(peers);
 | 
				
			||||||
            GivenThereIsAConfiguration(configuration);
 | 
					            GivenThereIsAConfiguration(configuration);
 | 
				
			||||||
            GivenFiveServersAreRunning();
 | 
					            GivenFiveServersAreRunning();
 | 
				
			||||||
            GivenIHaveAnOcelotToken("/administration");
 | 
					            await GivenIHaveAnOcelotToken("/administration");
 | 
				
			||||||
            WhenISendACommandIntoTheCluster(command);
 | 
					            await WhenISendACommandIntoTheCluster(command);
 | 
				
			||||||
            Thread.Sleep(5000);
 | 
					            Thread.Sleep(5000);
 | 
				
			||||||
            ThenTheCommandIsReplicatedToAllStateMachines(command);
 | 
					            await ThenTheCommandIsReplicatedToAllStateMachines(command);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        [Fact(Skip = "still broken waiting for work in rafty")]
 | 
					        [Fact]
 | 
				
			||||||
        public void should_persist_command_to_five_servers_when_using_administration_api()
 | 
					        public async Task should_persist_command_to_five_servers_when_using_administration_api()
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
 | 
					            var peers = new List<FilePeer>
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                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 configuration = new FileConfiguration
 | 
				
			||||||
            { 
 | 
					            { 
 | 
				
			||||||
            };
 | 
					            };
 | 
				
			||||||
@@ -154,17 +179,29 @@ namespace Ocelot.IntegrationTests
 | 
				
			|||||||
            };
 | 
					            };
 | 
				
			||||||
             
 | 
					             
 | 
				
			||||||
            var command = new UpdateFileConfiguration(updatedConfiguration);
 | 
					            var command = new UpdateFileConfiguration(updatedConfiguration);
 | 
				
			||||||
 | 
					            GivenThePeersAre(peers);
 | 
				
			||||||
            GivenThereIsAConfiguration(configuration);
 | 
					            GivenThereIsAConfiguration(configuration);
 | 
				
			||||||
            GivenFiveServersAreRunning();
 | 
					            GivenFiveServersAreRunning();
 | 
				
			||||||
            GivenIHaveAnOcelotToken("/administration");
 | 
					            await GivenIHaveAnOcelotToken("/administration");
 | 
				
			||||||
            GivenIHaveAddedATokenToMyRequest();
 | 
					            GivenIHaveAddedATokenToMyRequest();
 | 
				
			||||||
            WhenIPostOnTheApiGateway("/administration/configuration", updatedConfiguration);
 | 
					            await WhenIPostOnTheApiGateway("/administration/configuration", updatedConfiguration);
 | 
				
			||||||
            ThenTheCommandIsReplicatedToAllStateMachines(command);
 | 
					            await ThenTheCommandIsReplicatedToAllStateMachines(command);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private void WhenISendACommandIntoTheCluster(UpdateFileConfiguration command)
 | 
					        private void GivenThePeersAre(List<FilePeer> peers)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            bool SendCommand()
 | 
					            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<bool> SendCommand()
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                try
 | 
					                try
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
@@ -174,13 +211,13 @@ namespace Ocelot.IntegrationTests
 | 
				
			|||||||
                        TypeNameHandling = TypeNameHandling.All
 | 
					                        TypeNameHandling = TypeNameHandling.All
 | 
				
			||||||
                    });
 | 
					                    });
 | 
				
			||||||
                    var httpContent = new StringContent(json);
 | 
					                    var httpContent = new StringContent(json);
 | 
				
			||||||
                    httpContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
 | 
					                    httpContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");
 | 
				
			||||||
                    using (var httpClient = new HttpClient())
 | 
					                    using (var httpClient = new HttpClient())
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken);
 | 
					                        httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken);
 | 
				
			||||||
                        var response = httpClient.PostAsync($"{p.HostAndPort}/administration/raft/command", httpContent).GetAwaiter().GetResult();
 | 
					                        var response = await httpClient.PostAsync($"{p.HostAndPort}/administration/raft/command", httpContent);
 | 
				
			||||||
                        response.EnsureSuccessStatusCode();
 | 
					                        response.EnsureSuccessStatusCode();
 | 
				
			||||||
                        var content = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
 | 
					                        var content = await response.Content.ReadAsStringAsync();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        var errorResult = JsonConvert.DeserializeObject<ErrorResponse<UpdateFileConfiguration>>(content);
 | 
					                        var errorResult = JsonConvert.DeserializeObject<ErrorResponse<UpdateFileConfiguration>>(content);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -206,13 +243,19 @@ namespace Ocelot.IntegrationTests
 | 
				
			|||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            var commandSent = WaitFor(20000).Until(() => SendCommand());
 | 
					            var commandSent = await WaitFor(40000).Until(async () =>
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                var result = await SendCommand();
 | 
				
			||||||
 | 
					                Thread.Sleep(1000);
 | 
				
			||||||
 | 
					                return result;
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            commandSent.ShouldBeTrue();   
 | 
					            commandSent.ShouldBeTrue();   
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private void ThenTheCommandIsReplicatedToAllStateMachines(UpdateFileConfiguration expecteds)
 | 
					        private async Task ThenTheCommandIsReplicatedToAllStateMachines(UpdateFileConfiguration expecteds)
 | 
				
			||||||
        {            
 | 
					        {            
 | 
				
			||||||
            bool CommandCalledOnAllStateMachines()
 | 
					            async Task<bool> CommandCalledOnAllStateMachines()
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                try
 | 
					                try
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
@@ -232,8 +275,8 @@ namespace Ocelot.IntegrationTests
 | 
				
			|||||||
                        }
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        _httpClientForAssertions.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken);
 | 
					                        _httpClientForAssertions.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken);
 | 
				
			||||||
                        var result = _httpClientForAssertions.GetAsync($"{peer.HostAndPort}/administration/configuration").Result;
 | 
					                        var result = await _httpClientForAssertions.GetAsync($"{peer.HostAndPort}/administration/configuration");
 | 
				
			||||||
                        var json = result.Content.ReadAsStringAsync().Result;
 | 
					                        var json = await result.Content.ReadAsStringAsync();
 | 
				
			||||||
                        var response = JsonConvert.DeserializeObject<FileConfiguration>(json, new JsonSerializerSettings{TypeNameHandling = TypeNameHandling.All});
 | 
					                        var response = JsonConvert.DeserializeObject<FileConfiguration>(json, new JsonSerializerSettings{TypeNameHandling = TypeNameHandling.All});
 | 
				
			||||||
                        response.GlobalConfiguration.RequestIdKey.ShouldBe(expecteds.Configuration.GlobalConfiguration.RequestIdKey);
 | 
					                        response.GlobalConfiguration.RequestIdKey.ShouldBe(expecteds.Configuration.GlobalConfiguration.RequestIdKey);
 | 
				
			||||||
                        response.GlobalConfiguration.ServiceDiscoveryProvider.Host.ShouldBe(expecteds.Configuration.GlobalConfiguration.ServiceDiscoveryProvider.Host);
 | 
					                        response.GlobalConfiguration.ServiceDiscoveryProvider.Host.ShouldBe(expecteds.Configuration.GlobalConfiguration.ServiceDiscoveryProvider.Host);
 | 
				
			||||||
@@ -268,19 +311,29 @@ namespace Ocelot.IntegrationTests
 | 
				
			|||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            var commandOnAllStateMachines = WaitFor(20000).Until(() => CommandCalledOnAllStateMachines());
 | 
					            var commandOnAllStateMachines = await WaitFor(40000).Until(async () =>
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                var result = await CommandCalledOnAllStateMachines();
 | 
				
			||||||
 | 
					                Thread.Sleep(1000);
 | 
				
			||||||
 | 
					                return result;
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            commandOnAllStateMachines.ShouldBeTrue();   
 | 
					            commandOnAllStateMachines.ShouldBeTrue();   
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private void WhenIPostOnTheApiGateway(string url, FileConfiguration updatedConfiguration)
 | 
					        private async Task WhenIPostOnTheApiGateway(string url, FileConfiguration updatedConfiguration)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            bool SendCommand()
 | 
					            async Task<bool> SendCommand()
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                var json = JsonConvert.SerializeObject(updatedConfiguration);
 | 
					                var json = JsonConvert.SerializeObject(updatedConfiguration);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                var content = new StringContent(json);
 | 
					                var content = new StringContent(json);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
 | 
					                content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
 | 
				
			||||||
                _response = _httpClient.PostAsync(url, content).Result;
 | 
					
 | 
				
			||||||
                var responseContent = _response.Content.ReadAsStringAsync().Result;
 | 
					                _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.")
 | 
					                if(responseContent == "There was a problem. This error message sucks raise an issue in GitHub.")
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
@@ -295,7 +348,13 @@ namespace Ocelot.IntegrationTests
 | 
				
			|||||||
                return _response.IsSuccessStatusCode;
 | 
					                return _response.IsSuccessStatusCode;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            var commandSent = WaitFor(20000).Until(() => SendCommand());
 | 
					            var commandSent = await WaitFor(40000).Until(async () =>
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                var result = await SendCommand();
 | 
				
			||||||
 | 
					                Thread.Sleep(1000);
 | 
				
			||||||
 | 
					                return result;
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            commandSent.ShouldBeTrue();
 | 
					            commandSent.ShouldBeTrue();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -304,9 +363,9 @@ namespace Ocelot.IntegrationTests
 | 
				
			|||||||
            _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken);
 | 
					            _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private void GivenIHaveAnOcelotToken(string adminPath)
 | 
					        private async Task GivenIHaveAnOcelotToken(string adminPath)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            bool AddToken()
 | 
					            async Task<bool> AddToken()
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                try
 | 
					                try
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
@@ -320,8 +379,8 @@ namespace Ocelot.IntegrationTests
 | 
				
			|||||||
                    };
 | 
					                    };
 | 
				
			||||||
                    var content = new FormUrlEncodedContent(formData);
 | 
					                    var content = new FormUrlEncodedContent(formData);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    var response = _httpClient.PostAsync(tokenUrl, content).Result;
 | 
					                    var response = await _httpClient.PostAsync(tokenUrl, content);
 | 
				
			||||||
                    var responseContent = response.Content.ReadAsStringAsync().Result;
 | 
					                    var responseContent = await response.Content.ReadAsStringAsync();
 | 
				
			||||||
                    if(!response.IsSuccessStatusCode)
 | 
					                    if(!response.IsSuccessStatusCode)
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        return false;
 | 
					                        return false;
 | 
				
			||||||
@@ -329,7 +388,7 @@ namespace Ocelot.IntegrationTests
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
                    _token = JsonConvert.DeserializeObject<BearerToken>(responseContent);
 | 
					                    _token = JsonConvert.DeserializeObject<BearerToken>(responseContent);
 | 
				
			||||||
                    var configPath = $"{adminPath}/.well-known/openid-configuration";
 | 
					                    var configPath = $"{adminPath}/.well-known/openid-configuration";
 | 
				
			||||||
                    response = _httpClient.GetAsync(configPath).Result;
 | 
					                    response = await _httpClient.GetAsync(configPath);
 | 
				
			||||||
                    return response.IsSuccessStatusCode;
 | 
					                    return response.IsSuccessStatusCode;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                catch(Exception)
 | 
					                catch(Exception)
 | 
				
			||||||
@@ -338,7 +397,13 @@ namespace Ocelot.IntegrationTests
 | 
				
			|||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            var addToken = WaitFor(20000).Until(() => AddToken());
 | 
					            var addToken = await WaitFor(40000).Until(async () =>
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                var result = await AddToken();
 | 
				
			||||||
 | 
					                Thread.Sleep(1000);
 | 
				
			||||||
 | 
					                return result;
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            addToken.ShouldBeTrue();   
 | 
					            addToken.ShouldBeTrue();   
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,6 +15,7 @@ using Ocelot.Configuration;
 | 
				
			|||||||
namespace Ocelot.UnitTests.Controllers
 | 
					namespace Ocelot.UnitTests.Controllers
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    using Ocelot.Configuration.Repository;
 | 
					    using Ocelot.Configuration.Repository;
 | 
				
			||||||
 | 
					    using Rafty.Concensus.Node;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public class FileConfigurationControllerTests
 | 
					    public class FileConfigurationControllerTests
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
@@ -126,14 +127,14 @@ namespace Ocelot.UnitTests.Controllers
 | 
				
			|||||||
        {
 | 
					        {
 | 
				
			||||||
            _node
 | 
					            _node
 | 
				
			||||||
                .Setup(x => x.Accept(It.IsAny<UpdateFileConfiguration>()))
 | 
					                .Setup(x => x.Accept(It.IsAny<UpdateFileConfiguration>()))
 | 
				
			||||||
                .ReturnsAsync(new Rafty.Concensus.OkResponse<UpdateFileConfiguration>(new UpdateFileConfiguration(new FileConfiguration())));
 | 
					                .ReturnsAsync(new Rafty.Infrastructure.OkResponse<UpdateFileConfiguration>(new UpdateFileConfiguration(new FileConfiguration())));
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private void GivenTheNodeReturnsError()
 | 
					        private void GivenTheNodeReturnsError()
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            _node
 | 
					            _node
 | 
				
			||||||
                .Setup(x => x.Accept(It.IsAny<UpdateFileConfiguration>()))
 | 
					                .Setup(x => x.Accept(It.IsAny<UpdateFileConfiguration>()))
 | 
				
			||||||
                .ReturnsAsync(new Rafty.Concensus.ErrorResponse<UpdateFileConfiguration>("error", new UpdateFileConfiguration(new FileConfiguration())));
 | 
					                .ReturnsAsync(new Rafty.Infrastructure.ErrorResponse<UpdateFileConfiguration>("error", new UpdateFileConfiguration(new FileConfiguration())));
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private void GivenTheConfigSetterReturns(Response response)
 | 
					        private void GivenTheConfigSetterReturns(Response response)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -49,7 +49,7 @@
 | 
				
			|||||||
    <PackageReference Include="TestStack.BDDfy" Version="4.3.2" />
 | 
					    <PackageReference Include="TestStack.BDDfy" Version="4.3.2" />
 | 
				
			||||||
    <PackageReference Include="xunit" Version="2.3.1" />
 | 
					    <PackageReference Include="xunit" Version="2.3.1" />
 | 
				
			||||||
    <PackageReference Include="Butterfly.Client.AspNetCore" Version="0.0.8" />
 | 
					    <PackageReference Include="Butterfly.Client.AspNetCore" Version="0.0.8" />
 | 
				
			||||||
    <PackageReference Include="Rafty" Version="0.4.3" />
 | 
					    <PackageReference Include="Rafty" Version="0.4.4" />
 | 
				
			||||||
  </ItemGroup>
 | 
					  </ItemGroup>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <ItemGroup>
 | 
					  <ItemGroup>
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user