Skip to content
This repository has been archived by the owner on Aug 3, 2024. It is now read-only.
/ ServerCommon Public archive

Add the Killswitch client #169

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions NuGet.Server.Common.sln
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NuGet.Services.Sql", "src\N
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NuGet.Services.Sql.Tests", "tests\NuGet.Services.Sql.Tests\NuGet.Services.Sql.Tests.csproj", "{FA2B3447-7242-495F-A8E1-D94181C0C9A5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NuGet.Services.KillSwitch", "src\NuGet.Services.KillSwitch\NuGet.Services.KillSwitch.csproj", "{BC8D8216-8A19-4402-9DA3-238D0162B0A6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NuGet.Services.Killswitch.Tests", "tests\NuGet.Services.Killswitch.Tests\NuGet.Services.Killswitch.Tests.csproj", "{4F73955E-3A98-4403-8BBE-43271FED1CA3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -168,6 +172,14 @@ Global
{FA2B3447-7242-495F-A8E1-D94181C0C9A5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FA2B3447-7242-495F-A8E1-D94181C0C9A5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FA2B3447-7242-495F-A8E1-D94181C0C9A5}.Release|Any CPU.Build.0 = Release|Any CPU
{BC8D8216-8A19-4402-9DA3-238D0162B0A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BC8D8216-8A19-4402-9DA3-238D0162B0A6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BC8D8216-8A19-4402-9DA3-238D0162B0A6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BC8D8216-8A19-4402-9DA3-238D0162B0A6}.Release|Any CPU.Build.0 = Release|Any CPU
{4F73955E-3A98-4403-8BBE-43271FED1CA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4F73955E-3A98-4403-8BBE-43271FED1CA3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4F73955E-3A98-4403-8BBE-43271FED1CA3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4F73955E-3A98-4403-8BBE-43271FED1CA3}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -198,6 +210,8 @@ Global
{E29F54DF-DFB8-4E27-940D-21ECCB9B6FC1} = {7783A106-0F4C-4055-9AB4-413FB2C7B8F0}
{F5121B0A-669F-48BD-86DC-27C546D1A825} = {8415FED7-1BED-4227-8B4F-BB7C24E041CD}
{FA2B3447-7242-495F-A8E1-D94181C0C9A5} = {7783A106-0F4C-4055-9AB4-413FB2C7B8F0}
{BC8D8216-8A19-4402-9DA3-238D0162B0A6} = {8415FED7-1BED-4227-8B4F-BB7C24E041CD}
{4F73955E-3A98-4403-8BBE-43271FED1CA3} = {7783A106-0F4C-4055-9AB4-413FB2C7B8F0}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {AA413DB0-5475-4B5D-A3AF-6323DA8D538B}
Expand Down
10 changes: 6 additions & 4 deletions build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,9 @@ Invoke-BuildStep 'Set version metadata in AssemblyInfo.cs' { `
"$PSScriptRoot\src\NuGet.Services.ServiceBus\Properties\AssemblyInfo.g.cs", `
"$PSScriptRoot\src\NuGet.Services.Validation\Properties\AssemblyInfo.g.cs", `
"$PSScriptRoot\src\NuGet.Services.Validation.Issues\Properties\AssemblyInfo.g.cs", `
"$PSScriptRoot\src\NuGet.Services.Sql\Properties\AssemblyInfo.g.cs"

"$PSScriptRoot\src\NuGet.Services.Sql\Properties\AssemblyInfo.g.cs", `
"$PSScriptRoot\src\NuGet.Services.Killswitch\Properties\AssemblyInfo.g.cs"

$versionMetadata | ForEach-Object {
Set-VersionInfo -Path $_ -Version $SimpleVersion -Branch $Branch -Commit $CommitSHA
}
Expand Down Expand Up @@ -105,8 +106,9 @@ Invoke-BuildStep 'Creating artifacts' { `
"src\NuGet.Services.ServiceBus\NuGet.Services.ServiceBus.csproj", `
"src\NuGet.Services.Validation\NuGet.Services.Validation.csproj", `
"src\NuGet.Services.Validation.Issues\NuGet.Services.Validation.Issues.csproj", `
"src\NuGet.Services.Sql\NuGet.Services.Sql.csproj"

"src\NuGet.Services.Sql\NuGet.Services.Sql.csproj", `
"src\NuGet.Services.Killswitch\NuGet.Services.Killswitch.csproj"

$projects | ForEach-Object {
New-Package (Join-Path $PSScriptRoot $_) -Configuration $Configuration -Symbols -IncludeReferencedProjects -MSBuildVersion "15"
}
Expand Down
128 changes: 128 additions & 0 deletions src/NuGet.Services.KillSwitch/HttpKillswitchClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;

namespace NuGet.Services.KillSwitch
{
/// <summary>
/// A killswitch service that queries an HTTP service periodically for the list of active killswitches.
/// </summary>
public class HttpKillswitchClient : IKillswitchClient
{
private readonly HttpClient _httpClient;
private readonly HttpKillswitchConfig _config;
private readonly ILogger<HttpKillswitchClient> _logger;

private int _started;
private KillswitchCache _cache;

public HttpKillswitchClient(
HttpClient httpClient,
HttpKillswitchConfig config,
ILogger<HttpKillswitchClient> logger)
{
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_config = config ?? throw new ArgumentNullException(nameof(config));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));

_started = 0;
_cache = null;
}

public async Task StartAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();

// Ensure that this client has not already been started.
if (Interlocked.CompareExchange(ref _started, 1, 0) != 0)
{
_logger.LogError("The killswitch client has already been started!");

throw new InvalidOperationException("The killswitch client has already been started!");
}

// Prime the killswitch cache.
await RefreshAsync(cancellationToken);

// Refresh the killswitch cache on a background thread periodically.
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
Task.Run(async () =>
{
while (!cancellationToken.IsCancellationRequested)
{
try
{
await Task.Delay(_config.RefreshInterval);
await RefreshAsync(cancellationToken);
}
catch (Exception e)
{
_logger.LogError(0, e, "Failed to refresh the killswitch cache due to exception");
}
}
});
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
}

public async Task RefreshAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();

_logger.LogInformation("Refreshing killswitches...");

var duration = Stopwatch.StartNew();
var response = await _httpClient.GetAsync(_config.KillswitchEndpoint, cancellationToken);
var content = await response.Content.ReadAsStringAsync();

var items = JsonConvert.DeserializeObject<List<string>>(content);

_cache = new KillswitchCache(items);

_logger.LogInformation("Refreshed killswitches after {ElapsedTime}", duration.Elapsed);
}

public bool IsActive(string name)
{
var cache = _cache;

if (cache == null)
{
_logger.LogError("Cannot check the killswitch {Name} as the client isn't started", name);

throw new InvalidOperationException("Cannot check the killswitch as the client isn't started");
}

if (cache.Staleness.Elapsed >= _config.MaximumStaleness)
{
_logger.LogError(
"Failed to determine whether the killswitch {Name} is active as the client's cache is {Staleness} stale",
name,
cache.Staleness.Elapsed);

throw new InvalidOperationException("Reached maximum killswitch staleness threshold");
}

return cache.Items.Contains(name);
}

private class KillswitchCache
{
public KillswitchCache(List<string> items)
{
Staleness = Stopwatch.StartNew();
Items = new HashSet<string>(items, StringComparer.OrdinalIgnoreCase);
}

public Stopwatch Staleness { get; }
public HashSet<string> Items { get; }
}
}
}
29 changes: 29 additions & 0 deletions src/NuGet.Services.KillSwitch/HttpKillswitchConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;

namespace NuGet.Services.KillSwitch
{
/// <summary>
/// The configuration for <see cref="HttpKillswitchClient"/>.
/// </summary>
public class HttpKillswitchConfig
{
/// <summary>
/// The REST endpoint that contains the list of activated killswitch.
/// </summary>
public Uri KillswitchEndpoint { get; set; }

/// <summary>
/// How frequently the endpoint should be queried.
/// </summary>
public TimeSpan RefreshInterval { get; set; } = TimeSpan.FromMinutes(5);

/// <summary>
/// The maximum allowed staleness by <see cref="HttpKillswitchClient.IsActive(string)"/>.
/// The method will throw if this threshold is reached.
/// </summary>
public TimeSpan MaximumStaleness { get; set; } = TimeSpan.FromMinutes(20);
}
}
39 changes: 39 additions & 0 deletions src/NuGet.Services.KillSwitch/IKillswitchClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Threading;
using System.Threading.Tasks;

namespace NuGet.Services.KillSwitch
{
/// <summary>
/// Use killswitches to dynamically disable features in your service.
/// </summary>
public interface IKillswitchClient
{
/// <summary>
/// Start tracking the killswitches. This method can be called only once per instance.
/// </summary>
/// <param name="cancellationToken">Token to stop tracking the killswitches.</param>
/// <returns>A task that completes after the killswitches have been loaded.</returns>
Task StartAsync(CancellationToken cancellationToken);

/// <summary>
/// Force a refresh of the list of activated killswitches. This can be called as frequently
/// as desired, regardless of whether <see cref="StartAsync(CancellationToken)"/> has been called.
/// </summary>
/// <param name="cancellationToken">Cancel the refresh operation.</param>
/// <returns>A task that completes when the known killswitches have been refreshed.</returns>
Task RefreshAsync(CancellationToken cancellationToken);

/// <summary>
/// Check whether the killswitch is known to be active. The list of known activated killswitches MUST be
/// loaded (through either <see cref="StartAsync(CancellationToken)"/> or <see cref="RefreshAsync"/>) prior
/// to calling this method. If you'd like to guarantee the result isn't stale, call <see cref="RefreshAsync"/>
/// prior to calling this method.
/// </summary>
/// <param name="name">The name of the killswitch.</param>
/// <returns>True if the killswitch is known to be active.</returns>
bool IsActive(string name);
}
}
53 changes: 53 additions & 0 deletions src/NuGet.Services.KillSwitch/NuGet.Services.KillSwitch.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{BC8D8216-8A19-4402-9DA3-238D0162B0A6}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>NuGet.Services.KillSwitch</RootNamespace>
<AssemblyName>NuGet.Services.KillSwitch</AssemblyName>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<TargetFrameworkProfile />
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="IKillswitchClient.cs" />
<Compile Include="HttpKillswitchConfig.cs" />
<Compile Include="HttpKillswitchClient.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<None Include="project.json" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="..\..\build\sign.targets" />
</Project>
15 changes: 15 additions & 0 deletions src/NuGet.Services.KillSwitch/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Reflection;
using System.Runtime.InteropServices;

[assembly: AssemblyTitle("NuGet.Services.Killswitch")]
[assembly: AssemblyDescription("Disable NuGet service features at runtime")]
[assembly: AssemblyProduct("NuGet.Services.Killswitch")]
[assembly: AssemblyCopyright("Copyright © .NET Foundation 2018")]
[assembly: ComVisible(false)]
[assembly: Guid("bc8d8216-8a19-4402-9da3-238d0162b0a6")]
[assembly: AssemblyCompany(".NET Foundation")]

// Assembly version info is set at build time
13 changes: 13 additions & 0 deletions src/NuGet.Services.KillSwitch/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"dependencies": {
"System.Net.Http": "4.3.3",
"Microsoft.Extensions.Logging": "1.1.2",
"Newtonsoft.Json": "9.0.1"
},
"frameworks": {
"net45": {}
},
"runtimes": {
"win": {}
}
}
Loading