refactoring service discovery and load balancing approach into load balancing middleware

This commit is contained in:
TomPallister 2017-02-01 22:00:01 +00:00
commit 2aa156d0a6
54 changed files with 1160 additions and 547 deletions

1
.gitignore vendored
View File

@ -235,3 +235,4 @@ _Pvt_Extensions
# FAKE - F# Make # FAKE - F# Make
.fake/ .fake/
tools/

4
GitVersion.yml Normal file
View File

@ -0,0 +1,4 @@
mode: ContinuousDelivery
branches: {}
ignore:
sha: []

View File

@ -9,18 +9,24 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
.gitignore = .gitignore .gitignore = .gitignore
appveyor.yml = appveyor.yml appveyor.yml = appveyor.yml
build-and-run-tests.bat = build-and-run-tests.bat build-and-release-unstable.ps1 = build-and-release-unstable.ps1
build.bat = build.bat build-and-run-tests.ps1 = build-and-run-tests.ps1
build.cake = build.cake
build.ps1 = build.ps1
build.readme.md = build.readme.md
configuration-explanation.txt = configuration-explanation.txt configuration-explanation.txt = configuration-explanation.txt
configuration.yaml = configuration.yaml
GitVersion.yml = GitVersion.yml
global.json = global.json global.json = global.json
LICENSE.md = LICENSE.md LICENSE.md = LICENSE.md
Ocelot.nuspec = Ocelot.nuspec Ocelot.nuspec = Ocelot.nuspec
push-to-nuget.bat = push-to-nuget.bat
README.md = README.md README.md = README.md
run-acceptance-tests.bat = run-acceptance-tests.bat release.ps1 = release.ps1
run-benchmarks.bat = run-benchmarks.bat ReleaseNotes.md = ReleaseNotes.md
run-tests.bat = run-tests.bat run-acceptance-tests.ps1 = run-acceptance-tests.ps1
run-unit-tests.bat = run-unit-tests.bat run-benchmarks.ps1 = run-benchmarks.ps1
run-unit-tests.ps1 = run-unit-tests.ps1
version.ps1 = version.ps1
EndProjectSection EndProjectSection
EndProject EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Ocelot", "src\Ocelot\Ocelot.xproj", "{D6DF4206-0DBA-41D8-884D-C3E08290FDBB}" Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Ocelot", "src\Ocelot\Ocelot.xproj", "{D6DF4206-0DBA-41D8-884D-C3E08290FDBB}"

1
ReleaseNotes.md Normal file
View File

@ -0,0 +1 @@
No issues closed since last release

View File

@ -3,10 +3,6 @@ configuration:
- Release - Release
platform: Any CPU platform: Any CPU
build_script: build_script:
- build.bat - build.ps1
test_script:
- run-tests.bat
after_test:
- push-to-nuget.bat %appveyor_build_version% %nugetApiKey%
cache: cache:
- '%USERPROFILE%\.nuget\packages' - '%USERPROFILE%\.nuget\packages'

View File

@ -0,0 +1 @@
./build.ps1 -target build-full

View File

@ -1,2 +0,0 @@
./run-tests.bat
./build.bat

1
build-and-run-tests.ps1 Normal file
View File

@ -0,0 +1 @@
./build.ps1 -target RunTests

View File

@ -1,8 +0,0 @@
echo -------------------------
echo Building Ocelot
dotnet restore src/Ocelot
dotnet build src/Ocelot -c Release

347
build.cake Normal file
View File

@ -0,0 +1,347 @@
#tool "nuget:?package=GitVersion.CommandLine"
#tool "nuget:?package=OpenCover"
#tool "nuget:?package=ReportGenerator"
#tool "nuget:?package=GitReleaseNotes"
#addin "nuget:?package=Cake.DoInDirectory"
#addin "nuget:?package=Cake.Json"
// compile
var compileConfig = Argument("configuration", "Release");
var projectJson = "./src/Ocelot/project.json";
// build artifacts
var artifactsDir = Directory("artifacts");
// unit testing
var artifactsForUnitTestsDir = artifactsDir + Directory("UnitTests");
var unitTestAssemblies = @"./test/Ocelot.UnitTests";
// acceptance testing
var artifactsForAcceptanceTestsDir = artifactsDir + Directory("AcceptanceTests");
var acceptanceTestAssemblies = @"./test/Ocelot.AcceptanceTests";
// benchmark testing
var artifactsForBenchmarkTestsDir = artifactsDir + Directory("BenchmarkTests");
var benchmarkTestAssemblies = @"./test/Ocelot.Benchmarks";
// packaging
var packagesDir = artifactsDir + Directory("Packages");
var releaseNotesFile = packagesDir + File("releasenotes.md");
var artifactsFile = packagesDir + File("artifacts.txt");
// unstable releases
var nugetFeedUnstableKey = EnvironmentVariable("nuget-apikey-unstable");
var nugetFeedUnstableUploadUrl = "https://www.nuget.org/api/v2/package";
var nugetFeedUnstableSymbolsUploadUrl = "https://www.nuget.org/api/v2/package";
// stable releases
var tagsUrl = "https://api.github.com/repos/tompallister/ocelot/releases/tags/";
var nugetFeedStableKey = EnvironmentVariable("nuget-apikey-stable");
var nugetFeedStableUploadUrl = "https://www.nuget.org/api/v2/package";
var nugetFeedStableSymbolsUploadUrl = "https://www.nuget.org/api/v2/package";
// internal build variables - don't change these.
var releaseTag = "";
var buildVersion = committedVersion;
var committedVersion = "0.0.0-dev";
var target = Argument("target", "Default");
Information("target is " +target);
Information("Build configuration is " + compileConfig);
Task("Default")
.IsDependentOn("Build");
Task("Build")
.IsDependentOn("RunTests")
.IsDependentOn("CreatePackages");
Task("BuildAndReleaseUnstable")
.IsDependentOn("Build")
.IsDependentOn("ReleasePackagesToUnstableFeed");
Task("Clean")
.Does(() =>
{
if (DirectoryExists(artifactsDir))
{
DeleteDirectory(artifactsDir, recursive:true);
}
CreateDirectory(artifactsDir);
});
Task("Version")
.Does(() =>
{
var nugetVersion = GetNuGetVersionForCommit();
Information("SemVer version number: " + nugetVersion);
if (AppVeyor.IsRunningOnAppVeyor)
{
Information("Persisting version number...");
PersistVersion(nugetVersion);
buildVersion = nugetVersion;
}
else
{
Information("We are not running on build server, so we won't persist the version number.");
}
});
Task("Restore")
.IsDependentOn("Clean")
.IsDependentOn("Version")
.Does(() =>
{
DotNetCoreRestore("./src");
DotNetCoreRestore("./test");
});
Task("RunUnitTests")
.IsDependentOn("Restore")
.Does(() =>
{
var buildSettings = new DotNetCoreTestSettings
{
Configuration = compileConfig,
};
EnsureDirectoryExists(artifactsForUnitTestsDir);
DotNetCoreTest(unitTestAssemblies, buildSettings);
});
Task("RunAcceptanceTests")
.IsDependentOn("Restore")
.Does(() =>
{
var buildSettings = new DotNetCoreTestSettings
{
Configuration = "Debug", //acceptance test config is hard-coded for debug
};
EnsureDirectoryExists(artifactsForAcceptanceTestsDir);
DoInDirectory("test/Ocelot.AcceptanceTests", () =>
{
DotNetCoreTest(".", buildSettings);
});
});
Task("RunBenchmarkTests")
.IsDependentOn("Restore")
.Does(() =>
{
var buildSettings = new DotNetCoreRunSettings
{
Configuration = compileConfig,
};
EnsureDirectoryExists(artifactsForBenchmarkTestsDir);
DoInDirectory(benchmarkTestAssemblies, () =>
{
DotNetCoreRun(".", "", buildSettings);
});
});
Task("RunTests")
.IsDependentOn("RunUnitTests")
.IsDependentOn("RunAcceptanceTests")
.Does(() =>
{
});
Task("CreatePackages")
.Does(() =>
{
EnsureDirectoryExists(packagesDir);
GenerateReleaseNotes();
var settings = new DotNetCorePackSettings
{
OutputDirectory = packagesDir,
NoBuild = true
};
DotNetCorePack(projectJson, settings);
System.IO.File.WriteAllLines(artifactsFile, new[]{
"nuget:Ocelot." + buildVersion + ".nupkg",
"nugetSymbols:Ocelot." + buildVersion + ".symbols.nupkg",
"releaseNotes:releasenotes.md"
});
if (AppVeyor.IsRunningOnAppVeyor)
{
var path = packagesDir.ToString() + @"/**/*";
foreach (var file in GetFiles(path))
{
AppVeyor.UploadArtifact(file.FullPath);
}
}
});
Task("ReleasePackagesToUnstableFeed")
.IsDependentOn("CreatePackages")
.Does(() =>
{
PublishPackages(nugetFeedUnstableKey, nugetFeedUnstableUploadUrl, nugetFeedUnstableSymbolsUploadUrl);
});
Task("EnsureStableReleaseRequirements")
.Does(() =>
{
if (!AppVeyor.IsRunningOnAppVeyor)
{
throw new Exception("Stable release should happen via appveyor");
}
var isTag =
AppVeyor.Environment.Repository.Tag.IsTag &&
!string.IsNullOrWhiteSpace(AppVeyor.Environment.Repository.Tag.Name);
if (!isTag)
{
throw new Exception("Stable release should happen from a published GitHub release");
}
});
Task("UpdateVersionInfo")
.IsDependentOn("EnsureStableReleaseRequirements")
.Does(() =>
{
releaseTag = AppVeyor.Environment.Repository.Tag.Name;
AppVeyor.UpdateBuildVersion(releaseTag);
});
Task("DownloadGitHubReleaseArtifacts")
.IsDependentOn("UpdateVersionInfo")
.Does(() =>
{
EnsureDirectoryExists(packagesDir);
var releaseUrl = tagsUrl + releaseTag;
var assets_url = ParseJson(GetResource(releaseUrl))
.GetValue("assets_url")
.Value<string>();
foreach(var asset in DeserializeJson<JArray>(GetResource(assets_url)))
{
var file = packagesDir + File(asset.Value<string>("name"));
Information("Downloading " + file);
DownloadFile(asset.Value<string>("browser_download_url"), file);
}
});
Task("ReleasePackagesToStableFeed")
.IsDependentOn("DownloadGitHubReleaseArtifacts")
.Does(() =>
{
PublishPackages(nugetFeedStableKey, nugetFeedStableUploadUrl, nugetFeedStableSymbolsUploadUrl);
});
Task("Release")
.IsDependentOn("ReleasePackagesToStableFeed");
RunTarget(target);
/// Gets nuique nuget version for this commit
private string GetNuGetVersionForCommit()
{
GitVersion(new GitVersionSettings{
UpdateAssemblyInfo = false,
OutputType = GitVersionOutput.BuildServer
});
var versionInfo = GitVersion(new GitVersionSettings{ OutputType = GitVersionOutput.Json });
return versionInfo.NuGetVersion;
}
/// Updates project version in all of our projects
private void PersistVersion(string version)
{
Information(string.Format("We'll search all project.json files for {0} and replace with {1}...", committedVersion, version));
var projectJsonFiles = GetFiles("./**/project.json");
foreach(var projectJsonFile in projectJsonFiles)
{
var file = projectJsonFile.ToString();
Information(string.Format("Updating {0}...", file));
var updatedProjectJson = System.IO.File.ReadAllText(file)
.Replace(committedVersion, version);
System.IO.File.WriteAllText(file, updatedProjectJson);
}
}
/// generates release notes based on issues closed in GitHub since the last release
private void GenerateReleaseNotes()
{
Information("Generating release notes at " + releaseNotesFile);
var releaseNotesExitCode = StartProcess(
@"tools/GitReleaseNotes/tools/gitreleasenotes.exe",
new ProcessSettings { Arguments = ". /o " + releaseNotesFile });
if (string.IsNullOrEmpty(System.IO.File.ReadAllText(releaseNotesFile)))
{
System.IO.File.WriteAllText(releaseNotesFile, "No issues closed since last release");
}
if (releaseNotesExitCode != 0)
{
throw new Exception("Failed to generate release notes");
}
}
/// Publishes code and symbols packages to nuget feed, based on contents of artifacts file
private void PublishPackages(string feedApiKey, string codeFeedUrl, string symbolFeedUrl)
{
var artifacts = System.IO.File
.ReadAllLines(artifactsFile)
.Select(l => l.Split(':'))
.ToDictionary(v => v[0], v => v[1]);
var codePackage = packagesDir + File(artifacts["nuget"]);
var symbolsPackage = packagesDir + File(artifacts["nugetSymbols"]);
NuGetPush(
codePackage,
new NuGetPushSettings {
ApiKey = feedApiKey,
Source = codeFeedUrl
});
NuGetPush(
symbolsPackage,
new NuGetPushSettings {
ApiKey = feedApiKey,
Source = symbolFeedUrl
});
}
/// gets the resource from the specified url
private string GetResource(string url)
{
Information("Getting resource from " + url);
var assetsRequest = System.Net.WebRequest.CreateHttp(url);
assetsRequest.Method = "GET";
assetsRequest.Accept = "application/vnd.github.v3+json";
assetsRequest.UserAgent = "BuildScript";
using (var assetsResponse = assetsRequest.GetResponse())
{
var assetsStream = assetsResponse.GetResponseStream();
var assetsReader = new StreamReader(assetsStream);
return assetsReader.ReadToEnd();
}
}

189
build.ps1 Normal file
View File

@ -0,0 +1,189 @@
##########################################################################
# This is the Cake bootstrapper script for PowerShell.
# This file was downloaded from https://github.com/cake-build/resources
# Feel free to change this file to fit your needs.
##########################################################################
<#
.SYNOPSIS
This is a Powershell script to bootstrap a Cake build.
.DESCRIPTION
This Powershell script will download NuGet if missing, restore NuGet tools (including Cake)
and execute your Cake build script with the parameters you provide.
.PARAMETER Script
The build script to execute.
.PARAMETER Target
The build script target to run.
.PARAMETER Configuration
The build configuration to use.
.PARAMETER Verbosity
Specifies the amount of information to be displayed.
.PARAMETER Experimental
Tells Cake to use the latest Roslyn release.
.PARAMETER WhatIf
Performs a dry run of the build script.
No tasks will be executed.
.PARAMETER Mono
Tells Cake to use the Mono scripting engine.
.PARAMETER SkipToolPackageRestore
Skips restoring of packages.
.PARAMETER ScriptArgs
Remaining arguments are added here.
.LINK
http://cakebuild.net
#>
[CmdletBinding()]
Param(
[string]$Script = "build.cake",
[string]$Target = "Default",
[ValidateSet("Release", "Debug")]
[string]$Configuration = "Release",
[ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")]
[string]$Verbosity = "Verbose",
[switch]$Experimental,
[Alias("DryRun","Noop")]
[switch]$WhatIf,
[switch]$Mono,
[switch]$SkipToolPackageRestore,
[Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)]
[string[]]$ScriptArgs
)
[Reflection.Assembly]::LoadWithPartialName("System.Security") | Out-Null
function MD5HashFile([string] $filePath)
{
if ([string]::IsNullOrEmpty($filePath) -or !(Test-Path $filePath -PathType Leaf))
{
return $null
}
[System.IO.Stream] $file = $null;
[System.Security.Cryptography.MD5] $md5 = $null;
try
{
$md5 = [System.Security.Cryptography.MD5]::Create()
$file = [System.IO.File]::OpenRead($filePath)
return [System.BitConverter]::ToString($md5.ComputeHash($file))
}
finally
{
if ($file -ne $null)
{
$file.Dispose()
}
}
}
Write-Host "Preparing to run build script..."
if(!$PSScriptRoot){
$PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent
}
$TOOLS_DIR = Join-Path $PSScriptRoot "tools"
$NUGET_EXE = Join-Path $TOOLS_DIR "nuget.exe"
$CAKE_EXE = Join-Path $TOOLS_DIR "Cake/Cake.exe"
$NUGET_URL = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe"
$PACKAGES_CONFIG = Join-Path $TOOLS_DIR "packages.config"
$PACKAGES_CONFIG_MD5 = Join-Path $TOOLS_DIR "packages.config.md5sum"
# Should we use mono?
$UseMono = "";
if($Mono.IsPresent) {
Write-Verbose -Message "Using the Mono based scripting engine."
$UseMono = "-mono"
}
# Should we use the new Roslyn?
$UseExperimental = "";
if($Experimental.IsPresent -and !($Mono.IsPresent)) {
Write-Verbose -Message "Using experimental version of Roslyn."
$UseExperimental = "-experimental"
}
# Is this a dry run?
$UseDryRun = "";
if($WhatIf.IsPresent) {
$UseDryRun = "-dryrun"
}
# Make sure tools folder exists
if ((Test-Path $PSScriptRoot) -and !(Test-Path $TOOLS_DIR)) {
Write-Verbose -Message "Creating tools directory..."
New-Item -Path $TOOLS_DIR -Type directory | out-null
}
# Make sure that packages.config exist.
if (!(Test-Path $PACKAGES_CONFIG)) {
Write-Verbose -Message "Downloading packages.config..."
try { (New-Object System.Net.WebClient).DownloadFile("http://cakebuild.net/download/bootstrapper/packages", $PACKAGES_CONFIG) } catch {
Throw "Could not download packages.config."
}
}
# Try find NuGet.exe in path if not exists
if (!(Test-Path $NUGET_EXE)) {
Write-Verbose -Message "Trying to find nuget.exe in PATH..."
$existingPaths = $Env:Path -Split ';' | Where-Object { (![string]::IsNullOrEmpty($_)) -and (Test-Path $_) }
$NUGET_EXE_IN_PATH = Get-ChildItem -Path $existingPaths -Filter "nuget.exe" | Select -First 1
if ($NUGET_EXE_IN_PATH -ne $null -and (Test-Path $NUGET_EXE_IN_PATH.FullName)) {
Write-Verbose -Message "Found in PATH at $($NUGET_EXE_IN_PATH.FullName)."
$NUGET_EXE = $NUGET_EXE_IN_PATH.FullName
}
}
# Try download NuGet.exe if not exists
if (!(Test-Path $NUGET_EXE)) {
Write-Verbose -Message "Downloading NuGet.exe..."
try {
(New-Object System.Net.WebClient).DownloadFile($NUGET_URL, $NUGET_EXE)
} catch {
Throw "Could not download NuGet.exe."
}
}
# Save nuget.exe path to environment to be available to child processed
$ENV:NUGET_EXE = $NUGET_EXE
# Restore tools from NuGet?
if(-Not $SkipToolPackageRestore.IsPresent) {
Push-Location
Set-Location $TOOLS_DIR
# Check for changes in packages.config and remove installed tools if true.
[string] $md5Hash = MD5HashFile($PACKAGES_CONFIG)
if((!(Test-Path $PACKAGES_CONFIG_MD5)) -Or
($md5Hash -ne (Get-Content $PACKAGES_CONFIG_MD5 ))) {
Write-Verbose -Message "Missing or changed package.config hash..."
Remove-Item * -Recurse -Exclude packages.config,nuget.exe
}
Write-Verbose -Message "Restoring tools from NuGet..."
$NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$TOOLS_DIR`""
if ($LASTEXITCODE -ne 0) {
Throw "An error occured while restoring NuGet tools."
}
else
{
$md5Hash | Out-File $PACKAGES_CONFIG_MD5 -Encoding "ASCII"
}
Write-Verbose -Message ($NuGetOutput | out-string)
Pop-Location
}
# Make sure that Cake has been installed.
if (!(Test-Path $CAKE_EXE)) {
Throw "Could not find Cake.exe at $CAKE_EXE"
}
# Start Cake
Write-Host "Running build script..."
Invoke-Expression "& `"$CAKE_EXE`" `"$Script`" -target=`"$Target`" -configuration=`"$Configuration`" -verbosity=`"$Verbosity`" $UseMono $UseDryRun $UseExperimental $ScriptArgs"
exit $LASTEXITCODE

22
build.readme.md Normal file
View File

@ -0,0 +1,22 @@
#1. Overview
This document summarises the build and release process for the project. The build scripts are written using [Cake](http://cakebuild.net/), and are defined in `./build.cake`. The scripts have been designed to be run by either developers locally or by a build server (currently [AppVeyor](https://www.appveyor.com/)), with minimal logic defined in the build server itself.
#2. Building
* You'll generally want to run the `./build.ps1` script. This will compile, run unit and acceptance tests and build the output packages locally. Output will got to the `./artifacts` directory.
* You can view the current commit's [SemVer](http://semver.org/) build information by running `./version.ps1`.
* The other `./*.ps1` scripts perform subsets of the build process, if you don't want to run the full build.
* The release process works best with GitFlow branching; this allows us to publish every development commit to an unstable feed with a unique SemVer version, and then choose when to release to a stable feed.
#3. Release process
This section defines the release process for the maintainers of the project.
* Merge pull requests to the `release` branch.
* Every commit pushed to the Origin repo will kick off the [ocelot-build](https://ci.appveyor.com/project/binarymash/ocelot) project in AppVeyor. This performs the same tasks as the command line build, and in addition pushes the packages to the unstable nuget feed.
* When you're ready for a release, create a release branch. You'll probably want to update the committed `./ReleaseNotes.md` based on the contents of the equivalent file in the `./artifacts` directory.
* When the `release` branch has built successfully in Appveyor, select the build and then Deploy to the `GitHub Release` environment. This will create a new release in GitHub.
* In Github, navigate to the [release](https://github.com/binarymash/Ocelot/releases). Modify the release name and tag as desired.
* When you're ready, publish the release. This will tag the commit with the specified release number.
* The [ocelot-release](https://ci.appveyor.com/project/binarymash/ocelot-wtaj9) project will detect the newly created tag and kick off the release process. This will download the artifacts from GitHub, and publish the packages to the stable nuget feed.
* When you have a final stable release build, merge the `release` branch into `master` and `develop`. Deploy the master branch to github and following the full release process as described above. Don't forget to uncheck the "This is a pre-release" checkbox in GitHub before publishing.
* Note - because the release builds are initiated by tagging a commit, if for some reason a release build fails in AppVeyor you'll need to delete the tag from the repo and republish the release in GitHub.

View File

@ -1,11 +0,0 @@
echo -------------------------
echo Packing Ocelot Version %1
nuget pack .\Ocelot.nuspec -version %1
echo Publishing Ocelot
nuget push Ocelot.%1.nupkg -ApiKey %2 -Source https://www.nuget.org/api/v2/package

1
release.ps1 Normal file
View File

@ -0,0 +1 @@
./build.ps1 -target Release

View File

@ -1,8 +0,0 @@
echo Running Ocelot.AcceptanceTests
cd test/Ocelot.AcceptanceTests/
dotnet restore
dotnet test
cd ../../
echo Restoring Ocelot.ManualTest
dotnet restore test/Ocelot.ManualTest/

1
run-acceptance-tests.ps1 Normal file
View File

@ -0,0 +1 @@
./build -target RunAcceptanceTests

View File

@ -1,15 +0,0 @@
echo -------------------------
echo Running Ocelot.Benchmarks
cd test/Ocelot.Benchmarks
dotnet restore
dotnet run
cd ../../

1
run-benchmarks.ps1 Normal file
View File

@ -0,0 +1 @@
./build.ps1 -target RunBenchmarkTests

View File

@ -1,2 +0,0 @@
./run-unit-tests.bat
./run-acceptance-tests.bat

View File

@ -1,8 +0,0 @@
echo -------------------------
echo Restoring Ocelot
dotnet restore src/Ocelot
echo Running Ocelot.UnitTests
dotnet restore test/Ocelot.UnitTests/
dotnet test test/Ocelot.UnitTests/

1
run-unit-tests.ps1 Normal file
View File

@ -0,0 +1 @@
./build.ps1 -target RunUnitTests

View File

@ -201,13 +201,12 @@ namespace Ocelot.Configuration.Builder
public ReRoute Build() public ReRoute Build()
{ {
Func<HostAndPort> downstreamHostFunc = () => new HostAndPort(_downstreamHost, _dsPort);
return new ReRoute(new DownstreamPathTemplate(_downstreamPathTemplate), _upstreamTemplate, _upstreamHttpMethod, _upstreamTemplatePattern, return new ReRoute(new DownstreamPathTemplate(_downstreamPathTemplate), _upstreamTemplate, _upstreamHttpMethod, _upstreamTemplatePattern,
_isAuthenticated, new AuthenticationOptions(_authenticationProvider, _authenticationProviderUrl, _scopeName, _isAuthenticated, new AuthenticationOptions(_authenticationProvider, _authenticationProviderUrl, _scopeName,
_requireHttps, _additionalScopes, _scopeSecret), _configHeaderExtractorProperties, _claimToClaims, _routeClaimRequirement, _requireHttps, _additionalScopes, _scopeSecret), _configHeaderExtractorProperties, _claimToClaims, _routeClaimRequirement,
_isAuthorised, _claimToQueries, _requestIdHeaderKey, _isCached, _fileCacheOptions, _serviceName, _isAuthorised, _claimToQueries, _requestIdHeaderKey, _isCached, _fileCacheOptions, _serviceName,
_useServiceDiscovery, _serviceDiscoveryAddress, _serviceDiscoveryProvider, downstreamHostFunc, _downstreamScheme, _loadBalancer); _useServiceDiscovery, _serviceDiscoveryAddress, _serviceDiscoveryProvider, _downstreamScheme, _loadBalancer,
_downstreamHost, _dsPort);
} }
} }
} }

View File

@ -6,6 +6,7 @@ using Microsoft.Extensions.Options;
using Ocelot.Configuration.File; using Ocelot.Configuration.File;
using Ocelot.Configuration.Parser; using Ocelot.Configuration.Parser;
using Ocelot.Configuration.Validator; using Ocelot.Configuration.Validator;
using Ocelot.LoadBalancer.LoadBalancers;
using Ocelot.Responses; using Ocelot.Responses;
using Ocelot.ServiceDiscovery; using Ocelot.ServiceDiscovery;
using Ocelot.Utilities; using Ocelot.Utilities;
@ -97,31 +98,6 @@ namespace Ocelot.Configuration.Creator
&& !string.IsNullOrEmpty(globalConfiguration?.ServiceDiscoveryProvider?.Address) && !string.IsNullOrEmpty(globalConfiguration?.ServiceDiscoveryProvider?.Address)
&& !string.IsNullOrEmpty(globalConfiguration?.ServiceDiscoveryProvider?.Provider); && !string.IsNullOrEmpty(globalConfiguration?.ServiceDiscoveryProvider?.Provider);
//can we do the logic in this func to get the host and port from the load balancer?
//lBfactory.GetLbForThisDownstreamTemplate
//use it in the func to get the next host and port?
//how do we release it? cant callback, could access the lb and release later?
//ideal world we would get the host and port, then make the request using it, then release the connection to the lb
Func<HostAndPort> downstreamHostAndPortFunc = () => {
//service provider factory takes the reRoute
//can return no service provider (just use ocelot config)
//can return consol service provider
//returns a service provider
//we call get on the service provider
//could reutrn services from consol or just configuration.json
//this returns a list of services and we take the first one
var hostAndPort = new HostAndPort(reRoute.DownstreamHost.Trim('/'), reRoute.DownstreamPort);
var services = new List<Service>();
var serviceProvider = new NoServiceProvider(services);
var service = serviceProvider.Get();
var firstHostAndPort = service[0].HostAndPort;
return firstHostAndPort;
};
if (isAuthenticated) if (isAuthenticated)
{ {
var authOptionsForRoute = new AuthenticationOptions(reRoute.AuthenticationOptions.Provider, var authOptionsForRoute = new AuthenticationOptions(reRoute.AuthenticationOptions.Provider,
@ -139,8 +115,8 @@ namespace Ocelot.Configuration.Creator
reRoute.RouteClaimsRequirement, isAuthorised, claimsToQueries, reRoute.RouteClaimsRequirement, isAuthorised, claimsToQueries,
requestIdKey, isCached, new CacheOptions(reRoute.FileCacheOptions.TtlSeconds), requestIdKey, isCached, new CacheOptions(reRoute.FileCacheOptions.TtlSeconds),
reRoute.ServiceName, useServiceDiscovery, globalConfiguration?.ServiceDiscoveryProvider?.Provider, reRoute.ServiceName, useServiceDiscovery, globalConfiguration?.ServiceDiscoveryProvider?.Provider,
globalConfiguration?.ServiceDiscoveryProvider?.Address, downstreamHostAndPortFunc, reRoute.DownstreamScheme, globalConfiguration?.ServiceDiscoveryProvider?.Address, reRoute.DownstreamScheme,
reRoute.LoadBalancer); reRoute.LoadBalancer, reRoute.DownstreamHost, reRoute.DownstreamPort);
} }
return new ReRoute(new DownstreamPathTemplate(reRoute.DownstreamPathTemplate), reRoute.UpstreamTemplate, return new ReRoute(new DownstreamPathTemplate(reRoute.DownstreamPathTemplate), reRoute.UpstreamTemplate,
@ -149,8 +125,8 @@ namespace Ocelot.Configuration.Creator
reRoute.RouteClaimsRequirement, isAuthorised, new List<ClaimToThing>(), reRoute.RouteClaimsRequirement, isAuthorised, new List<ClaimToThing>(),
requestIdKey, isCached, new CacheOptions(reRoute.FileCacheOptions.TtlSeconds), requestIdKey, isCached, new CacheOptions(reRoute.FileCacheOptions.TtlSeconds),
reRoute.ServiceName, useServiceDiscovery, globalConfiguration?.ServiceDiscoveryProvider?.Provider, reRoute.ServiceName, useServiceDiscovery, globalConfiguration?.ServiceDiscoveryProvider?.Provider,
globalConfiguration?.ServiceDiscoveryProvider?.Address, downstreamHostAndPortFunc, reRoute.DownstreamScheme, globalConfiguration?.ServiceDiscoveryProvider?.Address, reRoute.DownstreamScheme,
reRoute.LoadBalancer); reRoute.LoadBalancer, reRoute.DownstreamHost, reRoute.DownstreamPort);
} }
private string BuildUpstreamTemplate(FileReRoute reRoute) private string BuildUpstreamTemplate(FileReRoute reRoute)

View File

@ -10,10 +10,12 @@ namespace Ocelot.Configuration
bool isAuthenticated, AuthenticationOptions authenticationOptions, List<ClaimToThing> configurationHeaderExtractorProperties, bool isAuthenticated, AuthenticationOptions authenticationOptions, List<ClaimToThing> configurationHeaderExtractorProperties,
List<ClaimToThing> claimsToClaims, Dictionary<string, string> routeClaimsRequirement, bool isAuthorised, List<ClaimToThing> claimsToQueries, List<ClaimToThing> claimsToClaims, Dictionary<string, string> routeClaimsRequirement, bool isAuthorised, List<ClaimToThing> claimsToQueries,
string requestIdKey, bool isCached, CacheOptions fileCacheOptions, string serviceName, bool useServiceDiscovery, string requestIdKey, bool isCached, CacheOptions fileCacheOptions, string serviceName, bool useServiceDiscovery,
string serviceDiscoveryProvider, string serviceDiscoveryAddress, Func<HostAndPort> downstreamHostAndPort, string serviceDiscoveryProvider, string serviceDiscoveryAddress,
string downstreamScheme, string loadBalancer) string downstreamScheme, string loadBalancer, string downstreamHost, int downstreamPort)
{ {
LoadBalancer = loadBalancer; LoadBalancer = loadBalancer;
DownstreamHost = downstreamHost;
DownstreamPort = downstreamPort;
DownstreamPathTemplate = downstreamPathTemplate; DownstreamPathTemplate = downstreamPathTemplate;
UpstreamTemplate = upstreamTemplate; UpstreamTemplate = upstreamTemplate;
UpstreamHttpMethod = upstreamHttpMethod; UpstreamHttpMethod = upstreamHttpMethod;
@ -35,7 +37,6 @@ namespace Ocelot.Configuration
UseServiceDiscovery = useServiceDiscovery; UseServiceDiscovery = useServiceDiscovery;
ServiceDiscoveryProvider = serviceDiscoveryProvider; ServiceDiscoveryProvider = serviceDiscoveryProvider;
ServiceDiscoveryAddress = serviceDiscoveryAddress; ServiceDiscoveryAddress = serviceDiscoveryAddress;
DownstreamHostAndPort = downstreamHostAndPort;
DownstreamScheme = downstreamScheme; DownstreamScheme = downstreamScheme;
} }
public DownstreamPathTemplate DownstreamPathTemplate { get; private set; } public DownstreamPathTemplate DownstreamPathTemplate { get; private set; }
@ -56,8 +57,9 @@ namespace Ocelot.Configuration
public bool UseServiceDiscovery { get; private set;} public bool UseServiceDiscovery { get; private set;}
public string ServiceDiscoveryProvider { get; private set;} public string ServiceDiscoveryProvider { get; private set;}
public string ServiceDiscoveryAddress { get; private set;} public string ServiceDiscoveryAddress { get; private set;}
public Func<HostAndPort> DownstreamHostAndPort {get;private set;}
public string DownstreamScheme {get;private set;} public string DownstreamScheme {get;private set;}
public string LoadBalancer {get;private set;} public string LoadBalancer {get;private set;}
public string DownstreamHost { get; private set; }
public int DownstreamPort { get; private set; }
} }
} }

View File

@ -6,6 +6,7 @@ using Ocelot.DownstreamUrlCreator.UrlTemplateReplacer;
using Ocelot.Infrastructure.RequestData; using Ocelot.Infrastructure.RequestData;
using Ocelot.Logging; using Ocelot.Logging;
using Ocelot.Middleware; using Ocelot.Middleware;
using Ocelot.Values;
namespace Ocelot.DownstreamUrlCreator.Middleware namespace Ocelot.DownstreamUrlCreator.Middleware
{ {
@ -46,14 +47,9 @@ namespace Ocelot.DownstreamUrlCreator.Middleware
var dsScheme = DownstreamRoute.ReRoute.DownstreamScheme; var dsScheme = DownstreamRoute.ReRoute.DownstreamScheme;
//here we could have a lb factory that takes stuff or we could just get the load balancer from the reRoute? //todo - get this out of scoped data repo?
//returns the lb for this request var dsHostAndPort = new HostAndPort(DownstreamRoute.ReRoute.DownstreamHost,
DownstreamRoute.ReRoute.DownstreamPort);
//lease the next address from the lb
//this could return the load balancer which you call next on, that gives you the host and port then you can call release in a try catch
//and if the call works?
var dsHostAndPort = DownstreamRoute.ReRoute.DownstreamHostAndPort();
var dsUrl = _urlBuilder.Build(dsPath.Data.Value, dsScheme, dsHostAndPort); var dsUrl = _urlBuilder.Build(dsPath.Data.Value, dsScheme, dsHostAndPort);

View File

@ -0,0 +1,11 @@
using Ocelot.Responses;
using Ocelot.Values;
namespace Ocelot.LoadBalancer.LoadBalancers
{
public interface ILoadBalancer
{
Response<HostAndPort> Lease();
Response Release(HostAndPort hostAndPort);
}
}

View File

@ -0,0 +1,7 @@
namespace Ocelot.LoadBalancer.LoadBalancers
{
public interface ILoadBalancerFactory
{
ILoadBalancer Get(string serviceName, string loadBalancer);
}
}

View File

@ -0,0 +1,15 @@
using Ocelot.Values;
namespace Ocelot.LoadBalancer.LoadBalancers
{
public class Lease
{
public Lease(HostAndPort hostAndPort, int connections)
{
HostAndPort = hostAndPort;
Connections = connections;
}
public HostAndPort HostAndPort { get; private set; }
public int Connections { get; private set; }
}
}

View File

@ -0,0 +1,139 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Ocelot.Errors;
using Ocelot.Responses;
using Ocelot.Values;
namespace Ocelot.LoadBalancer.LoadBalancers
{
public class LeastConnectionLoadBalancer : ILoadBalancer
{
private Func<List<Service>> _services;
private List<Lease> _leases;
private string _serviceName;
public LeastConnectionLoadBalancer(Func<List<Service>> services, string serviceName)
{
_services = services;
_serviceName = serviceName;
_leases = new List<Lease>();
}
public Response<HostAndPort> Lease()
{
var services = _services();
if (services == null)
{
return new ErrorResponse<HostAndPort>(new List<Error>() { new ServicesAreNullError($"services were null for {_serviceName}") });
}
if (!services.Any())
{
return new ErrorResponse<HostAndPort>(new List<Error>() { new ServicesAreEmptyError($"services were empty for {_serviceName}") });
}
//todo - maybe this should be moved somewhere else...? Maybe on a repeater on seperate thread? loop every second and update or something?
UpdateServices(services);
var leaseWithLeastConnections = GetLeaseWithLeastConnections();
_leases.Remove(leaseWithLeastConnections);
leaseWithLeastConnections = AddConnection(leaseWithLeastConnections);
_leases.Add(leaseWithLeastConnections);
return new OkResponse<HostAndPort>(new HostAndPort(leaseWithLeastConnections.HostAndPort.DownstreamHost, leaseWithLeastConnections.HostAndPort.DownstreamPort));
}
public Response Release(HostAndPort hostAndPort)
{
var matchingLease = _leases.FirstOrDefault(l => l.HostAndPort.DownstreamHost == hostAndPort.DownstreamHost
&& l.HostAndPort.DownstreamPort == hostAndPort.DownstreamPort);
if (matchingLease != null)
{
var replacementLease = new Lease(hostAndPort, matchingLease.Connections - 1);
_leases.Remove(matchingLease);
_leases.Add(replacementLease);
}
return new OkResponse();
}
private Lease AddConnection(Lease lease)
{
return new Lease(lease.HostAndPort, lease.Connections + 1);
}
private Lease GetLeaseWithLeastConnections()
{
//now get the service with the least connections?
Lease leaseWithLeastConnections = null;
for (var i = 0; i < _leases.Count; i++)
{
if (i == 0)
{
leaseWithLeastConnections = _leases[i];
}
else
{
if (_leases[i].Connections < leaseWithLeastConnections.Connections)
{
leaseWithLeastConnections = _leases[i];
}
}
}
return leaseWithLeastConnections;
}
private Response UpdateServices(List<Service> services)
{
if (_leases.Count > 0)
{
var leasesToRemove = new List<Lease>();
foreach (var lease in _leases)
{
var match = services.FirstOrDefault(s => s.HostAndPort.DownstreamHost == lease.HostAndPort.DownstreamHost
&& s.HostAndPort.DownstreamPort == lease.HostAndPort.DownstreamPort);
if (match == null)
{
leasesToRemove.Add(lease);
}
}
foreach (var lease in leasesToRemove)
{
_leases.Remove(lease);
}
foreach (var service in services)
{
var exists = _leases.FirstOrDefault(l => l.HostAndPort.ToString() == service.HostAndPort.ToString());
if (exists == null)
{
_leases.Add(new Lease(service.HostAndPort, 0));
}
}
}
else
{
foreach (var service in services)
{
_leases.Add(new Lease(service.HostAndPort, 0));
}
}
return new OkResponse();
}
}
}

View File

@ -0,0 +1,27 @@
using Ocelot.ServiceDiscovery;
namespace Ocelot.LoadBalancer.LoadBalancers
{
public class LoadBalancerFactory : ILoadBalancerFactory
{
private readonly IServiceProvider _serviceProvider;
public LoadBalancerFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public ILoadBalancer Get(string serviceName, string loadBalancer)
{
switch (loadBalancer)
{
case "RoundRobin":
return new RoundRobinLoadBalancer(_serviceProvider.Get());
case "LeastConnection":
return new LeastConnectionLoadBalancer(() => _serviceProvider.Get(), serviceName);
default:
return new NoLoadBalancer(_serviceProvider.Get());
}
}
}
}

View File

@ -0,0 +1,28 @@
using System.Collections.Generic;
using System.Linq;
using Ocelot.Responses;
using Ocelot.Values;
namespace Ocelot.LoadBalancer.LoadBalancers
{
public class NoLoadBalancer : ILoadBalancer
{
private readonly List<Service> _services;
public NoLoadBalancer(List<Service> services)
{
_services = services;
}
public Response<HostAndPort> Lease()
{
var service = _services.FirstOrDefault();
return new OkResponse<HostAndPort>(service.HostAndPort);
}
public Response Release(HostAndPort hostAndPort)
{
return new OkResponse();
}
}
}

View File

@ -0,0 +1,34 @@
using System.Collections.Generic;
using Ocelot.Responses;
using Ocelot.Values;
namespace Ocelot.LoadBalancer.LoadBalancers
{
public class RoundRobinLoadBalancer : ILoadBalancer
{
private readonly List<Service> _services;
private int _last;
public RoundRobinLoadBalancer(List<Service> services)
{
_services = services;
}
public Response<HostAndPort> Lease()
{
if (_last >= _services.Count)
{
_last = 0;
}
var next = _services[_last];
_last++;
return new OkResponse<HostAndPort>(next.HostAndPort);
}
public Response Release(HostAndPort hostAndPort)
{
return new OkResponse();
}
}
}

View File

@ -0,0 +1,12 @@
using Ocelot.Errors;
namespace Ocelot.LoadBalancer.LoadBalancers
{
public class ServicesAreEmptyError : Error
{
public ServicesAreEmptyError(string message)
: base(message, OcelotErrorCode.ServicesAreEmptyError)
{
}
}
}

View File

@ -0,0 +1,12 @@
using Ocelot.Errors;
namespace Ocelot.LoadBalancer.LoadBalancers
{
public class ServicesAreNullError : Error
{
public ServicesAreNullError(string message)
: base(message, OcelotErrorCode.ServicesAreNullError)
{
}
}
}

View File

@ -0,0 +1,67 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Ocelot.Infrastructure.RequestData;
using Ocelot.LoadBalancer.LoadBalancers;
using Ocelot.Logging;
using Ocelot.Middleware;
using Ocelot.QueryStrings.Middleware;
using Ocelot.ServiceDiscovery;
namespace Ocelot.LoadBalancer.Middleware
{
public class LoadBalancingMiddleware : OcelotMiddleware
{
private readonly RequestDelegate _next;
private readonly IOcelotLogger _logger;
public LoadBalancingMiddleware(RequestDelegate next,
IOcelotLoggerFactory loggerFactory,
IRequestScopedDataRepository requestScopedDataRepository)
: base(requestScopedDataRepository)
{
_next = next;
_logger = loggerFactory.CreateLogger<QueryStringBuilderMiddleware>();
}
public async Task Invoke(HttpContext context)
{
_logger.LogDebug("started calling query string builder middleware");
//todo - get out of di? or do this when we bootstrap?
var serviceProviderFactory = new ServiceProviderFactory();
var serviceConfig = new ServiceConfiguraion(
DownstreamRoute.ReRoute.ServiceName,
DownstreamRoute.ReRoute.DownstreamHost,
DownstreamRoute.ReRoute.DownstreamPort,
DownstreamRoute.ReRoute.UseServiceDiscovery);
//todo - get this out of some kind of service provider house?
var serviceProvider = serviceProviderFactory.Get(serviceConfig);
//todo - get out of di? or do this when we bootstrap?
var loadBalancerFactory = new LoadBalancerFactory(serviceProvider);
//todo - currently instanciates a load balancer per request which is wrong,
//need some kind of load balance house! :)
var loadBalancer = loadBalancerFactory.Get(DownstreamRoute.ReRoute.ServiceName, DownstreamRoute.ReRoute.LoadBalancer);
var response = loadBalancer.Lease();
_logger.LogDebug("calling next middleware");
//todo - try next middleware if we get an exception make sure we release
//the host and port? Not sure if this is the way to go but we shall see!
try
{
await _next.Invoke(context);
loadBalancer.Release(response.Data);
}
catch (Exception exception)
{
loadBalancer.Release(response.Data);
throw;
}
_logger.LogDebug("succesfully called next middleware");
}
}
}

View File

@ -3,11 +3,11 @@ using Ocelot.Values;
namespace Ocelot.ServiceDiscovery namespace Ocelot.ServiceDiscovery
{ {
public class NoServiceProvider : IServiceProvider public class ConfigurationServiceProvider : IServiceProvider
{ {
private List<Service> _services; private List<Service> _services;
public NoServiceProvider(List<Service> services) public ConfigurationServiceProvider(List<Service> services)
{ {
_services = services; _services = services;
} }

View File

@ -1,9 +1,7 @@
using Ocelot.Configuration;
namespace Ocelot.ServiceDiscovery namespace Ocelot.ServiceDiscovery
{ {
public interface IServiceProviderFactory public interface IServiceProviderFactory
{ {
Ocelot.ServiceDiscovery.IServiceProvider Get(ReRoute reRoute); IServiceProvider Get(ServiceConfiguraion serviceConfig);
} }
} }

View File

@ -1,13 +1,34 @@
using System; using System.Collections.Generic;
using Ocelot.Configuration; using Ocelot.Values;
namespace Ocelot.ServiceDiscovery namespace Ocelot.ServiceDiscovery
{ {
public class ServiceProviderFactory : IServiceProviderFactory public class ServiceProviderFactory : IServiceProviderFactory
{ {
public Ocelot.ServiceDiscovery.IServiceProvider Get(ReRoute reRoute) public IServiceProvider Get(ServiceConfiguraion serviceConfig)
{ {
throw new NotImplementedException(); var services = new List<Service>()
{
new Service(serviceConfig.ServiceName, new HostAndPort(serviceConfig.DownstreamHost, serviceConfig.DownstreamPort))
};
return new ConfigurationServiceProvider(services);
} }
} }
public class ServiceConfiguraion
{
public ServiceConfiguraion(string serviceName, string downstreamHost, int downstreamPort, bool useServiceDiscovery)
{
ServiceName = serviceName;
DownstreamHost = downstreamHost;
DownstreamPort = downstreamPort;
UseServiceDiscovery = useServiceDiscovery;
}
public string ServiceName { get; }
public string DownstreamHost { get; }
public int DownstreamPort { get; }
public bool UseServiceDiscovery { get; }
}
} }

View File

@ -4,16 +4,11 @@
{ {
public HostAndPort(string downstreamHost, int downstreamPort) public HostAndPort(string downstreamHost, int downstreamPort)
{ {
DownstreamHost = downstreamHost; DownstreamHost = downstreamHost?.Trim('/');
DownstreamPort = downstreamPort; DownstreamPort = downstreamPort;
} }
public string DownstreamHost { get; private set; } public string DownstreamHost { get; private set; }
public int DownstreamPort { get; private set; } public int DownstreamPort { get; private set; }
public override string ToString()
{
return $"{DownstreamHost}:{DownstreamPort}";
}
} }
} }

View File

@ -1,5 +1,5 @@
{ {
"version": "1.0.0-*", "version": "0.0.0-dev",
"dependencies": { "dependencies": {
"Microsoft.AspNetCore.Server.IISIntegration": "1.1.0", "Microsoft.AspNetCore.Server.IISIntegration": "1.1.0",

View File

@ -1 +1 @@
{"ReRoutes":[{"DownstreamPathTemplate":"/","UpstreamTemplate":"/","UpstreamHttpMethod":"Get","AuthenticationOptions":{"Provider":null,"ProviderRootUrl":null,"ScopeName":null,"RequireHttps":false,"AdditionalScopes":[],"ScopeSecret":null},"AddHeadersToRequest":{},"AddClaimsToRequest":{},"RouteClaimsRequirement":{},"AddQueriesToRequest":{},"RequestIdKey":null,"FileCacheOptions":{"TtlSeconds":0},"ReRouteIsCaseSensitive":false,"ServiceName":null,"DownstreamScheme":"http","DownstreamHost":"localhost","DownstreamPort":41879}],"GlobalConfiguration":{"RequestIdKey":null,"ServiceDiscoveryProvider":{"Provider":null,"Address":null}}} {"ReRoutes":[{"DownstreamPathTemplate":"41879/","UpstreamTemplate":"/","UpstreamHttpMethod":"Get","AuthenticationOptions":{"Provider":null,"ProviderRootUrl":null,"ScopeName":null,"RequireHttps":false,"AdditionalScopes":[],"ScopeSecret":null},"AddHeadersToRequest":{},"AddClaimsToRequest":{},"RouteClaimsRequirement":{},"AddQueriesToRequest":{},"RequestIdKey":null,"FileCacheOptions":{"TtlSeconds":0},"ReRouteIsCaseSensitive":false,"ServiceName":null,"DownstreamScheme":"http","DownstreamHost":"localhost","DownstreamPort":41879,"LoadBalancer":null}],"GlobalConfiguration":{"RequestIdKey":null,"ServiceDiscoveryProvider":{"Provider":null,"Address":null}}}

View File

@ -1,5 +1,5 @@
{ {
"version": "1.0.0-*", "version": "0.0.0-dev",
"buildOptions": { "buildOptions": {
"copyToOutput": { "copyToOutput": {
@ -22,10 +22,10 @@
"Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0", "Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0",
"Microsoft.AspNetCore.Http": "1.1.0", "Microsoft.AspNetCore.Http": "1.1.0",
"Microsoft.DotNet.InternalAbstractions": "1.0.0", "Microsoft.DotNet.InternalAbstractions": "1.0.0",
"Ocelot": "1.0.0-*", "Ocelot": "0.0.0-dev",
"xunit": "2.2.0-beta2-build3300", "xunit": "2.2.0-beta2-build3300",
"dotnet-test-xunit": "2.2.0-preview2-build1029", "dotnet-test-xunit": "2.2.0-preview2-build1029",
"Ocelot.ManualTest": "1.0.0-*", "Ocelot.ManualTest": "0.0.0-dev",
"Microsoft.AspNetCore.TestHost": "1.1.0", "Microsoft.AspNetCore.TestHost": "1.1.0",
"IdentityServer4": "1.0.1", "IdentityServer4": "1.0.1",
"Microsoft.AspNetCore.Mvc": "1.1.0", "Microsoft.AspNetCore.Mvc": "1.1.0",
@ -36,7 +36,7 @@
}, },
"runtimes": { "runtimes": {
"win10-x64": {}, "win10-x64": {},
"osx.10.11-x64":{}, "osx.10.11-x64": {},
"win7-x64": {} "win7-x64": {}
}, },
"frameworks": { "frameworks": {

View File

@ -1,11 +1,11 @@
{ {
"version": "1.0.0-*", "version": "0.0.0-dev",
"buildOptions": { "buildOptions": {
"emitEntryPoint": true "emitEntryPoint": true
}, },
"dependencies": { "dependencies": {
"Ocelot": "1.0.0-*", "Ocelot": "0.0.0-dev",
"BenchmarkDotNet": "0.10.1" "BenchmarkDotNet": "0.10.1"
}, },
"runtimes": { "runtimes": {

View File

@ -1,5 +1,5 @@
{ {
"version": "1.0.0-*", "version": "0.0.0-dev",
"dependencies": { "dependencies": {
"Microsoft.AspNetCore.Http": "1.1.0", "Microsoft.AspNetCore.Http": "1.1.0",
@ -10,7 +10,7 @@
"Microsoft.Extensions.Logging.Console": "1.1.0", "Microsoft.Extensions.Logging.Console": "1.1.0",
"Microsoft.Extensions.Logging.Debug": "1.1.0", "Microsoft.Extensions.Logging.Debug": "1.1.0",
"Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0", "Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0",
"Ocelot": "1.0.0-*", "Ocelot": "0.0.0-dev",
"Microsoft.AspNetCore.Server.Kestrel": "1.1.0", "Microsoft.AspNetCore.Server.Kestrel": "1.1.0",
"Microsoft.NETCore.App": "1.1.0" "Microsoft.NETCore.App": "1.1.0"
}, },

View File

@ -1,14 +1,12 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using Ocelot.LoadBalancer.LoadBalancers;
using Ocelot.Errors;
using Ocelot.Responses; using Ocelot.Responses;
using Ocelot.Values; using Ocelot.Values;
using Shouldly; using Shouldly;
using TestStack.BDDfy; using TestStack.BDDfy;
using Xunit; using Xunit;
namespace Ocelot.UnitTests namespace Ocelot.UnitTests.LoadBalancer
{ {
public class LeastConnectionTests public class LeastConnectionTests
{ {
@ -17,10 +15,6 @@ namespace Ocelot.UnitTests
private LeastConnectionLoadBalancer _leastConnection; private LeastConnectionLoadBalancer _leastConnection;
private List<Service> _services; private List<Service> _services;
public LeastConnectionTests()
{
}
[Fact] [Fact]
public void should_get_next_url() public void should_get_next_url()
{ {
@ -202,161 +196,4 @@ namespace Ocelot.UnitTests
_result.Data.DownstreamPort.ShouldBe(_hostAndPort.DownstreamPort); _result.Data.DownstreamPort.ShouldBe(_hostAndPort.DownstreamPort);
} }
} }
public class LeastConnectionLoadBalancer : ILoadBalancer
{
private Func<List<Service>> _services;
private List<Lease> _leases;
private string _serviceName;
public LeastConnectionLoadBalancer(Func<List<Service>> services, string serviceName)
{
_services = services;
_serviceName = serviceName;
_leases = new List<Lease>();
}
public Response<HostAndPort> Lease()
{
var services = _services();
if(services == null)
{
return new ErrorResponse<HostAndPort>(new List<Error>(){ new ServicesAreNullError($"services were null for {_serviceName}")});
}
if(!services.Any())
{
return new ErrorResponse<HostAndPort>(new List<Error>(){ new ServicesAreEmptyError($"services were empty for {_serviceName}")});
}
//todo - maybe this should be moved somewhere else...? Maybe on a repeater on seperate thread? loop every second and update or something?
UpdateServices(services);
var leaseWithLeastConnections = GetLeaseWithLeastConnections();
_leases.Remove(leaseWithLeastConnections);
leaseWithLeastConnections = AddConnection(leaseWithLeastConnections);
_leases.Add(leaseWithLeastConnections);
return new OkResponse<HostAndPort>(new HostAndPort(leaseWithLeastConnections.HostAndPort.DownstreamHost, leaseWithLeastConnections.HostAndPort.DownstreamPort));
}
public Response Release(HostAndPort hostAndPort)
{
var matchingLease = _leases.FirstOrDefault(l => l.HostAndPort.DownstreamHost == hostAndPort.DownstreamHost
&& l.HostAndPort.DownstreamPort == hostAndPort.DownstreamPort);
if(matchingLease != null)
{
var replacementLease = new Lease(hostAndPort, matchingLease.Connections - 1);
_leases.Remove(matchingLease);
_leases.Add(replacementLease);
}
return new OkResponse();
}
private Lease AddConnection(Lease lease)
{
return new Lease(lease.HostAndPort, lease.Connections + 1);
}
private Lease GetLeaseWithLeastConnections()
{
//now get the service with the least connections?
Lease leaseWithLeastConnections = null;
for(var i = 0; i < _leases.Count; i++)
{
if(i == 0)
{
leaseWithLeastConnections = _leases[i];
}
else
{
if(_leases[i].Connections < leaseWithLeastConnections.Connections)
{
leaseWithLeastConnections = _leases[i];
}
}
}
return leaseWithLeastConnections;
}
private Response UpdateServices(List<Service> services)
{
if(_leases.Count > 0)
{
var leasesToRemove = new List<Lease>();
foreach(var lease in _leases)
{
var match = services.FirstOrDefault(s => s.HostAndPort.DownstreamHost == lease.HostAndPort.DownstreamHost
&& s.HostAndPort.DownstreamPort == lease.HostAndPort.DownstreamPort);
if(match == null)
{
leasesToRemove.Add(lease);
}
}
foreach(var lease in leasesToRemove)
{
_leases.Remove(lease);
}
foreach(var service in services)
{
var exists = _leases.FirstOrDefault(l => l.HostAndPort.ToString() == service.HostAndPort.ToString());
if(exists == null)
{
_leases.Add(new Lease(service.HostAndPort, 0));
}
}
}
else
{
foreach(var service in services)
{
_leases.Add(new Lease(service.HostAndPort, 0));
}
}
return new OkResponse();
}
}
public class Lease
{
public Lease(HostAndPort hostAndPort, int connections)
{
HostAndPort = hostAndPort;
Connections = connections;
}
public HostAndPort HostAndPort {get;private set;}
public int Connections {get;private set;}
}
public class ServicesAreNullError : Error
{
public ServicesAreNullError(string message)
: base(message, OcelotErrorCode.ServicesAreNullError)
{
}
}
public class ServicesAreEmptyError : Error
{
public ServicesAreEmptyError(string message)
: base(message, OcelotErrorCode.ServicesAreEmptyError)
{
}
}
} }

View File

@ -0,0 +1,97 @@
using Moq;
using Ocelot.Configuration;
using Ocelot.Configuration.Builder;
using Ocelot.LoadBalancer.LoadBalancers;
using Ocelot.ServiceDiscovery;
using Shouldly;
using TestStack.BDDfy;
using Xunit;
namespace Ocelot.UnitTests.LoadBalancer
{
public class LoadBalancerFactoryTests
{
private ReRoute _reRoute;
private LoadBalancerFactory _factory;
private ILoadBalancer _result;
private Mock<IServiceProvider> _serviceProvider;
public LoadBalancerFactoryTests()
{
_serviceProvider = new Mock<IServiceProvider>();
_factory = new LoadBalancerFactory(_serviceProvider.Object);
}
[Fact]
public void should_return_no_load_balancer()
{
var reRoute = new ReRouteBuilder()
.Build();
this.Given(x => x.GivenAReRoute(reRoute))
.When(x => x.WhenIGetTheLoadBalancer())
.Then(x => x.ThenTheLoadBalancerIsReturned<NoLoadBalancer>())
.BDDfy();
}
[Fact]
public void should_return_round_robin_load_balancer()
{
var reRoute = new ReRouteBuilder()
.WithLoadBalancer("RoundRobin")
.Build();
this.Given(x => x.GivenAReRoute(reRoute))
.When(x => x.WhenIGetTheLoadBalancer())
.Then(x => x.ThenTheLoadBalancerIsReturned<RoundRobinLoadBalancer>())
.BDDfy();
}
[Fact]
public void should_return_round_least_connection_balancer()
{
var reRoute = new ReRouteBuilder()
.WithLoadBalancer("LeastConnection")
.Build();
this.Given(x => x.GivenAReRoute(reRoute))
.When(x => x.WhenIGetTheLoadBalancer())
.Then(x => x.ThenTheLoadBalancerIsReturned<LeastConnectionLoadBalancer>())
.BDDfy();
}
[Fact]
public void should_call_service_provider()
{
var reRoute = new ReRouteBuilder()
.WithLoadBalancer("RoundRobin")
.Build();
this.Given(x => x.GivenAReRoute(reRoute))
.When(x => x.WhenIGetTheLoadBalancer())
.Then(x => x.ThenTheServiceProviderIsCalledCorrectly())
.BDDfy();
}
private void ThenTheServiceProviderIsCalledCorrectly()
{
_serviceProvider
.Verify(x => x.Get(), Times.Once);
}
private void GivenAReRoute(ReRoute reRoute)
{
_reRoute = reRoute;
}
private void WhenIGetTheLoadBalancer()
{
_result = _factory.Get(_reRoute.ServiceName, _reRoute.LoadBalancer);
}
private void ThenTheLoadBalancerIsReturned<T>()
{
_result.ShouldBeOfType<T>();
}
}
}

View File

@ -0,0 +1,48 @@
using System.Collections.Generic;
using Ocelot.LoadBalancer.LoadBalancers;
using Ocelot.Responses;
using Ocelot.Values;
using Shouldly;
using TestStack.BDDfy;
using Xunit;
namespace Ocelot.UnitTests.LoadBalancer
{
public class NoLoadBalancerTests
{
private List<Service> _services;
private NoLoadBalancer _loadBalancer;
private Response<HostAndPort> _result;
[Fact]
public void should_return_host_and_port()
{
var hostAndPort = new HostAndPort("127.0.0.1", 80);
var services = new List<Service>
{
new Service("product", hostAndPort)
};
this.Given(x => x.GivenServices(services))
.When(x => x.WhenIGetTheNextHostAndPort())
.Then(x => x.ThenTheHostAndPortIs(hostAndPort))
.BDDfy();
}
private void GivenServices(List<Service> services)
{
_services = services;
}
private void WhenIGetTheNextHostAndPort()
{
_loadBalancer = new NoLoadBalancer(_services);
_result = _loadBalancer.Lease();
}
private void ThenTheHostAndPortIs(HostAndPort expected)
{
_result.Data.ShouldBe(expected);
}
}
}

View File

@ -1,12 +1,13 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using Ocelot.LoadBalancer.LoadBalancers;
using Ocelot.Responses; using Ocelot.Responses;
using Ocelot.Values; using Ocelot.Values;
using Shouldly; using Shouldly;
using TestStack.BDDfy; using TestStack.BDDfy;
using Xunit; using Xunit;
namespace Ocelot.UnitTests namespace Ocelot.UnitTests.LoadBalancer
{ {
public class RoundRobinTests public class RoundRobinTests
{ {
@ -64,38 +65,4 @@ namespace Ocelot.UnitTests
_hostAndPort.Data.ShouldBe(_services[index].HostAndPort); _hostAndPort.Data.ShouldBe(_services[index].HostAndPort);
} }
} }
public interface ILoadBalancer
{
Response<HostAndPort> Lease();
Response Release(HostAndPort hostAndPort);
}
public class RoundRobinLoadBalancer : ILoadBalancer
{
private readonly List<Service> _services;
private int _last;
public RoundRobinLoadBalancer(List<Service> services)
{
_services = services;
}
public Response<HostAndPort> Lease()
{
if (_last >= _services.Count)
{
_last = 0;
}
var next = _services[_last];
_last++;
return new OkResponse<HostAndPort>(next.HostAndPort);
}
public Response Release(HostAndPort hostAndPort)
{
return new OkResponse();
}
}
} }

View File

@ -1,187 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Moq;
using Ocelot.Configuration;
using Ocelot.Configuration.Builder;
using Ocelot.Responses;
using Ocelot.Values;
using Shouldly;
using TestStack.BDDfy;
using Xunit;
namespace Ocelot.UnitTests
{
public class LoadBalancerFactoryTests
{
private ReRoute _reRoute;
private LoadBalancerFactory _factory;
private ILoadBalancer _result;
private Mock<Ocelot.ServiceDiscovery.IServiceProvider> _serviceProvider;
public LoadBalancerFactoryTests()
{
_serviceProvider = new Mock<Ocelot.ServiceDiscovery.IServiceProvider>();
_factory = new LoadBalancerFactory(_serviceProvider.Object);
}
[Fact]
public void should_return_no_load_balancer()
{
var reRoute = new ReRouteBuilder()
.Build();
this.Given(x => x.GivenAReRoute(reRoute))
.When(x => x.WhenIGetTheLoadBalancer())
.Then(x => x.ThenTheLoadBalancerIsReturned<NoLoadBalancer>())
.BDDfy();
}
[Fact]
public void should_return_round_robin_load_balancer()
{
var reRoute = new ReRouteBuilder()
.WithLoadBalancer("RoundRobin")
.Build();
this.Given(x => x.GivenAReRoute(reRoute))
.When(x => x.WhenIGetTheLoadBalancer())
.Then(x => x.ThenTheLoadBalancerIsReturned<RoundRobinLoadBalancer>())
.BDDfy();
}
[Fact]
public void should_return_round_least_connection_balancer()
{
var reRoute = new ReRouteBuilder()
.WithLoadBalancer("LeastConnection")
.Build();
this.Given(x => x.GivenAReRoute(reRoute))
.When(x => x.WhenIGetTheLoadBalancer())
.Then(x => x.ThenTheLoadBalancerIsReturned<LeastConnectionLoadBalancer>())
.BDDfy();
}
[Fact]
public void should_call_service_provider()
{
var reRoute = new ReRouteBuilder()
.WithLoadBalancer("RoundRobin")
.Build();
this.Given(x => x.GivenAReRoute(reRoute))
.When(x => x.WhenIGetTheLoadBalancer())
.Then(x => x.ThenTheServiceProviderIsCalledCorrectly(reRoute))
.BDDfy();
}
private void ThenTheServiceProviderIsCalledCorrectly(ReRoute reRoute)
{
_serviceProvider
.Verify(x => x.Get(), Times.Once);
}
private void GivenAReRoute(ReRoute reRoute)
{
_reRoute = reRoute;
}
private void WhenIGetTheLoadBalancer()
{
_result = _factory.Get(_reRoute);
}
private void ThenTheLoadBalancerIsReturned<T>()
{
_result.ShouldBeOfType<T>();
}
}
public class NoLoadBalancerTests
{
private List<Service> _services;
private NoLoadBalancer _loadBalancer;
private Response<HostAndPort> _result;
[Fact]
public void should_return_host_and_port()
{
var hostAndPort = new HostAndPort("127.0.0.1", 80);
var services = new List<Service>
{
new Service("product", hostAndPort)
};
this.Given(x => x.GivenServices(services))
.When(x => x.WhenIGetTheNextHostAndPort())
.Then(x => x.ThenTheHostAndPortIs(hostAndPort))
.BDDfy();
}
private void GivenServices(List<Service> services)
{
_services = services;
}
private void WhenIGetTheNextHostAndPort()
{
_loadBalancer = new NoLoadBalancer(_services);
_result = _loadBalancer.Lease();
}
private void ThenTheHostAndPortIs(HostAndPort expected)
{
_result.Data.ShouldBe(expected);
}
}
public class NoLoadBalancer : ILoadBalancer
{
private List<Service> _services;
public NoLoadBalancer(List<Service> services)
{
_services = services;
}
public Response<HostAndPort> Lease()
{
var service = _services.FirstOrDefault();
return new OkResponse<HostAndPort>(service.HostAndPort);
}
public Response Release(HostAndPort hostAndPort)
{
return new OkResponse();
}
}
public interface ILoadBalancerFactory
{
ILoadBalancer Get(ReRoute reRoute);
}
public class LoadBalancerFactory : ILoadBalancerFactory
{
private Ocelot.ServiceDiscovery.IServiceProvider _serviceProvider;
public LoadBalancerFactory(Ocelot.ServiceDiscovery.IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public ILoadBalancer Get(ReRoute reRoute)
{
switch (reRoute.LoadBalancer)
{
case "RoundRobin":
return new RoundRobinLoadBalancer(_serviceProvider.Get());
case "LeastConnection":
return new LeastConnectionLoadBalancer(() => _serviceProvider.Get(), reRoute.ServiceName);
default:
return new NoLoadBalancer(_serviceProvider.Get());
}
}
}
}

View File

@ -1,17 +1,15 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using Ocelot.Configuration;
using Ocelot.ServiceDiscovery; using Ocelot.ServiceDiscovery;
using Ocelot.Values; using Ocelot.Values;
using Shouldly; using Shouldly;
using TestStack.BDDfy; using TestStack.BDDfy;
using Xunit; using Xunit;
namespace Ocelot.UnitTests namespace Ocelot.UnitTests.ServiceDiscovery
{ {
public class NoServiceProviderTests public class ConfigurationServiceProviderTests
{ {
private NoServiceProvider _serviceProvider; private ConfigurationServiceProvider _serviceProvider;
private HostAndPort _hostAndPort; private HostAndPort _hostAndPort;
private List<Service> _result; private List<Service> _result;
private List<Service> _expected; private List<Service> _expected;
@ -26,20 +24,20 @@ namespace Ocelot.UnitTests
new Service("product", hostAndPort) new Service("product", hostAndPort)
}; };
this.Given(x => x.GivenAHostAndPort(services)) this.Given(x => x.GivenServices(services))
.When(x => x.WhenIGetTheService()) .When(x => x.WhenIGetTheService())
.Then(x => x.ThenTheFollowingIsReturned(services)) .Then(x => x.ThenTheFollowingIsReturned(services))
.BDDfy(); .BDDfy();
} }
private void GivenAHostAndPort(List<Service> services) private void GivenServices(List<Service> services)
{ {
_expected = services; _expected = services;
} }
private void WhenIGetTheService() private void WhenIGetTheService()
{ {
_serviceProvider = new NoServiceProvider(_expected); _serviceProvider = new ConfigurationServiceProvider(_expected);
_result = _serviceProvider.Get(); _result = _serviceProvider.Get();
} }

View File

@ -1,17 +1,15 @@
using Ocelot.Configuration;
using Ocelot.Configuration.Builder;
using Ocelot.ServiceDiscovery; using Ocelot.ServiceDiscovery;
using Shouldly; using Shouldly;
using TestStack.BDDfy; using TestStack.BDDfy;
using Xunit; using Xunit;
namespace Ocelot.UnitTests namespace Ocelot.UnitTests.ServiceDiscovery
{ {
public class ServiceProviderFactoryTests public class ServiceProviderFactoryTests
{ {
private ReRoute _reRote; private ServiceConfiguraion _serviceConfig;
private IServiceProvider _result; private IServiceProvider _result;
private ServiceProviderFactory _factory; private readonly ServiceProviderFactory _factory;
public ServiceProviderFactoryTests() public ServiceProviderFactoryTests()
{ {
@ -21,25 +19,22 @@ namespace Ocelot.UnitTests
[Fact] [Fact]
public void should_return_no_service_provider() public void should_return_no_service_provider()
{ {
var reRoute = new ReRouteBuilder() var serviceConfig = new ServiceConfiguraion("product", "127.0.0.1", 80, false);
.WithDownstreamHost("127.0.0.1")
.WithDownstreamPort(80)
.Build();
this.Given(x => x.GivenTheReRoute(reRoute)) this.Given(x => x.GivenTheReRoute(serviceConfig))
.When(x => x.WhenIGetTheServiceProvider()) .When(x => x.WhenIGetTheServiceProvider())
.Then(x => x.ThenTheServiceProviderIs<NoServiceProvider>()) .Then(x => x.ThenTheServiceProviderIs<ConfigurationServiceProvider>())
.BDDfy(); .BDDfy();
} }
private void GivenTheReRoute(ReRoute reRoute) private void GivenTheReRoute(ServiceConfiguraion serviceConfig)
{ {
_reRote = reRoute; _serviceConfig = serviceConfig;
} }
private void WhenIGetTheServiceProvider() private void WhenIGetTheServiceProvider()
{ {
_result = _factory.Get(_reRote); _result = _factory.Get(_serviceConfig);
} }
private void ThenTheServiceProviderIs<T>() private void ThenTheServiceProviderIs<T>()

View File

@ -4,7 +4,7 @@ using Shouldly;
using TestStack.BDDfy; using TestStack.BDDfy;
using Xunit; using Xunit;
namespace Ocelot.UnitTests namespace Ocelot.UnitTests.ServiceDiscovery
{ {
public class ServiceRegistryTests public class ServiceRegistryTests
{ {

View File

@ -1,5 +1,5 @@
{ {
"version": "1.0.0-*", "version": "0.0.0-dev",
"testRunner": "xunit", "testRunner": "xunit",
@ -13,7 +13,7 @@
"Microsoft.Extensions.Logging.Debug": "1.1.0", "Microsoft.Extensions.Logging.Debug": "1.1.0",
"Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0", "Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0",
"Microsoft.AspNetCore.Http": "1.1.0", "Microsoft.AspNetCore.Http": "1.1.0",
"Ocelot": "1.0.0-*", "Ocelot": "0.0.0-dev",
"xunit": "2.2.0-beta2-build3300", "xunit": "2.2.0-beta2-build3300",
"dotnet-test-xunit": "2.2.0-preview2-build1029", "dotnet-test-xunit": "2.2.0-preview2-build1029",
"Moq": "4.6.38-alpha", "Moq": "4.6.38-alpha",

1
version.ps1 Normal file
View File

@ -0,0 +1 @@
.\tools\GitVersion.CommandLine\tools\GitVersion.exe