Skip to content

Commit

Permalink
prepare 5.6.2 release (#101)
Browse files Browse the repository at this point in the history
  • Loading branch information
eli-darkly committed Mar 26, 2019
1 parent d294a78 commit 2d3707a
Show file tree
Hide file tree
Showing 8 changed files with 246 additions and 72 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

All notable changes to the LaunchDarkly .NET SDK will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org).

## [5.6.2] - 2019-03-26
### Changed:
- The default value for the configuration property `capacity` (maximum number of events that can be stored at once) is now 10000, consistent with the other SDKs, rather than 500.

### Fixed:
- Under some circumstances, a `CancellationTokenSource` might not be disposed of after making an HTTP request, which could cause a timer object to be leaked. ([#100](https://github.com/launchdarkly/dotnet-client/issues/100))
- In polling mode, if the client received an HTTP error it would retry the same request one second later. This was inconsistent with the other SDKs; the correct behavior is for it to wait until the next scheduled poll.
- The `HttpClientTimeout` configuration property was being ignored when making HTTP requests to send analytics events.

## [5.6.1] - 2019-01-14
### Fixed:
- The assemblies in this package now have Authenticode signatures.
Expand Down
41 changes: 41 additions & 0 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
jobs:
- job: netstandard2_0
pool:
vmImage: 'vs2017-win2016'
steps:
- task: PowerShell@2
displayName: 'Build'
inputs:
targetType: inline
workingDirectory: $(System.DefaultWorkingDirectory)
script: |
dotnet --version
dotnet restore
dotnet build src/LaunchDarkly.Client -f netstandard2.0
- task: PowerShell@2
displayName: 'Test'
inputs:
targetType: inline
workingDirectory: $(System.DefaultWorkingDirectory)
script: |
dotnet test test/LaunchDarkly.Tests/LaunchDarkly.Tests.csproj -f netcoreapp2.0
- job: net45
pool:
vmImage: 'vs2017-win2016'
steps:
- task: PowerShell@2
displayName: 'Build'
inputs:
targetType: inline
workingDirectory: $(System.DefaultWorkingDirectory)
script: |
dotnet --version
dotnet restore
dotnet build src/LaunchDarkly.Client -f net45
- task: PowerShell@2
displayName: 'Test'
inputs:
targetType: inline
workingDirectory: $(System.DefaultWorkingDirectory)
script: |
dotnet test test/LaunchDarkly.Tests/LaunchDarkly.Tests.csproj -f net46
2 changes: 1 addition & 1 deletion src/LaunchDarkly.Client/Configuration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ public class Configuration : IBaseConfiguration
/// <summary>
/// Default value for <see cref="EventQueueCapacity"/>.
/// </summary>
private static readonly int DefaultEventQueueCapacity = 500;
private static readonly int DefaultEventQueueCapacity = 10000;
/// <summary>
/// Default value for <see cref="EventQueueFrequency"/>.
/// </summary>
Expand Down
13 changes: 11 additions & 2 deletions src/LaunchDarkly.Client/FeatureFlag.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,17 @@ internal EvalResult(EvaluationDetail<JToken> result, IList<FeatureRequestEvent>
Result = result;
PrerequisiteEvents = events;
}
}

}

int IFlagEventProperties.EventVersion => Version;

// This method is called by EventFactory to determine if extra tracking should be
// enabled for an event, based on the evaluation reason. It is not enabled yet.
bool IFlagEventProperties.IsExperiment(EvaluationReason reason)
{
return false;
}

internal EvalResult Evaluate(User user, IFeatureStore featureStore, EventFactory eventFactory)
{
IList<FeatureRequestEvent> prereqEvents = new List<FeatureRequestEvent>();
Expand Down
100 changes: 36 additions & 64 deletions src/LaunchDarkly.Client/FeatureRequestor.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
Expand Down Expand Up @@ -48,56 +47,59 @@ private void Dispose(bool disposing)
// was a problem getting flags.
async Task<AllData> IFeatureRequestor.GetAllDataAsync()
{
var cts = new CancellationTokenSource(_config.HttpClientTimeout);
string content = null;
content = await Get(cts, _allUri);
if (string.IsNullOrEmpty(content)) {
return null;
var ret = await GetAsync<AllData>(_allUri);
if (ret != null)
{
Log.DebugFormat("Get all returned {0} feature flags and {1} segments",
ret.Flags.Keys.Count, ret.Segments.Keys.Count);
}
var ret = JsonConvert.DeserializeObject<AllData>(content);

Log.DebugFormat("Get all returned {0} feature flags and {1} segments",
ret.Flags.Keys.Count, ret.Segments.Keys.Count);

return ret;
}

// Returns the latest version of a flag, or null if it has not been modified. Throws an exception if there
// was a problem getting flags.
async Task<FeatureFlag> IFeatureRequestor.GetFlagAsync(string featureKey)
{
return await GetObjectAsync<FeatureFlag>(featureKey, "feature flag", typeof(FeatureFlag), _flagsUri);
return await GetAsync<FeatureFlag>(new Uri(_flagsUri, featureKey));
}

// Returns the latest version of a segment, or null if it has not been modified. Throws an exception if there
// was a problem getting segments.
async Task<Segment> IFeatureRequestor.GetSegmentAsync(string segmentKey)
{
return await GetObjectAsync<Segment>(segmentKey, "segment", typeof(Segment), _segmentsUri);
return await GetAsync<Segment>(new Uri(_segmentsUri, segmentKey));
}

internal async Task<T> GetObjectAsync<T>(string key, string objectName, Type objectType, Uri uriBase) where T : class
private async Task<T> GetAsync<T>(Uri path) where T : class
{
var cts = new CancellationTokenSource(_config.HttpClientTimeout);
string content = null;
Uri apiPath = new Uri(uriBase + key);
try
Log.DebugFormat("Getting flags with uri: {0}", path.AbsoluteUri);
var request = new HttpRequestMessage(HttpMethod.Get, path);
if (_etag != null)
{
content = await Get(cts, apiPath);
return (content == null) ? null : (T) JsonConvert.DeserializeObject(content, objectType);
request.Headers.IfNoneMatch.Add(_etag);
}
catch (Exception e)
{
Log.DebugFormat("Error getting {0}: {1} waiting 1 second before retrying.",
e, objectName, Util.ExceptionMessage(e));

System.Threading.Tasks.Task.Delay(TimeSpan.FromSeconds(1)).Wait();
cts = new CancellationTokenSource(_config.HttpClientTimeout);
try
{
content = await Get(cts, apiPath);
return (content == null) ? null : (T)JsonConvert.DeserializeObject(content, objectType);
}
using (var cts = new CancellationTokenSource(_config.HttpClientTimeout))
{
try
{
using (var response = await _httpClient.SendAsync(request, cts.Token).ConfigureAwait(false))
{
if (response.StatusCode == HttpStatusCode.NotModified)
{
Log.Debug("Get all flags returned 304: not modified");
return null;
}
_etag = response.Headers.ETag;
//We ensure the status code after checking for 304, because 304 isn't considered success
if (!response.IsSuccessStatusCode)
{
throw new UnsuccessfulResponseException((int)response.StatusCode);
}
var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
return string.IsNullOrEmpty(content) ? null : (T)JsonConvert.DeserializeObject<T>(content);
}
}
catch (TaskCanceledException tce)
{
if (tce.CancellationToken == cts.Token)
Expand All @@ -106,39 +108,9 @@ internal async Task<T> GetObjectAsync<T>(string key, string objectName, Type obj
throw;
}
//Otherwise this was a request timeout.
throw new TimeoutException("Get item with URL: " + apiPath +
throw new TimeoutException("Get item with URL: " + path.AbsoluteUri +
" timed out after : " + _config.HttpClientTimeout);
}
catch (Exception)
{
throw;
}
}
}

private async Task<string> Get(CancellationTokenSource cts, Uri path)
{
Log.DebugFormat("Getting flags with uri: {0}", path.AbsoluteUri);
var request = new HttpRequestMessage(HttpMethod.Get, path);
if (_etag != null)
{
request.Headers.IfNoneMatch.Add(_etag);
}

using (var response = await _httpClient.SendAsync(request, cts.Token).ConfigureAwait(false))
{
if (response.StatusCode == HttpStatusCode.NotModified)
{
Log.Debug("Get all flags returned 304: not modified");
return null;
}
_etag = response.Headers.ETag;
//We ensure the status code after checking for 304, because 304 isn't considered success
if (!response.IsSuccessStatusCode)
{
throw new UnsuccessfulResponseException((int)response.StatusCode);
}
return await response.Content.ReadAsStringAsync().ConfigureAwait(false);
}
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/LaunchDarkly.Client/LaunchDarkly.Client.csproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Version>5.6.1</Version>
<Version>5.6.2</Version>
<TargetFrameworks>netstandard1.4;netstandard1.6;netstandard2.0;net45</TargetFrameworks>
<PackageLicenseUrl>https://raw.githubusercontent.com/launchdarkly/.net-client/master/LICENSE</PackageLicenseUrl>
<DebugType>portable</DebugType>
Expand All @@ -15,10 +15,10 @@
<GenerateAssemblyCopyrightAttribute>false</GenerateAssemblyCopyrightAttribute>
</PropertyGroup>
<ItemGroup Condition="'$(Configuration)'=='Release'">
<PackageReference Include="LaunchDarkly.Common.StrongName" Version="1.2.3" />
<PackageReference Include="LaunchDarkly.Common.StrongName" Version="2.0.0" />
</ItemGroup>
<ItemGroup Condition="'$(Configuration)'=='Debug'">
<PackageReference Include="LaunchDarkly.Common" Version="1.2.3" />
<PackageReference Include="LaunchDarkly.Common" Version="2.0.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Common.Logging" Version="3.4.1" />
Expand Down
142 changes: 142 additions & 0 deletions test/LaunchDarkly.Tests/FeatureRequestorTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
using LaunchDarkly.Client;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using WireMock;
using WireMock.Logging;
using WireMock.RequestBuilders;
using WireMock.ResponseBuilders;
using WireMock.Server;
using Xunit;

namespace LaunchDarkly.Tests
{
public class FeatureRequestorTest : IDisposable
{
private const string AllDataJson = @"{""flags"":{""flag1"":{""key"":""flag1"",""version"":1}},""segments"":{""seg1"":{""key"":""seg1"",""version"":2}}}";

private FluentMockServer _server;
private IFeatureRequestor _requestor;

public FeatureRequestorTest()
{
_server = FluentMockServer.Start();
var config = Configuration.Default("key").WithUri(_server.Urls[0]).WithHttpClientTimeout(TimeSpan.FromDays(1));
_requestor = new FeatureRequestor(config);
}

void IDisposable.Dispose()
{
_server.Stop();
}

[Fact]
public async Task GetAllUsesCorrectUriAndParsesResponseAsync()
{
_server.Given(Request.Create().UsingGet())
.RespondWith(Response.Create().WithStatusCode(200).WithBody(AllDataJson));
var result = await _requestor.GetAllDataAsync();

var req = GetLastRequest();
Assert.Equal("/sdk/latest-all", req.Path);

Assert.Equal(1, result.Flags.Count);
Assert.Equal(1, result.Flags["flag1"].Version);
Assert.Equal(1, result.Segments.Count);
Assert.Equal(2, result.Segments["seg1"].Version);
}

[Fact]
public async Task GetAllStoresAndSendsEtag()
{
var etag = @"""abc123"""; // note that etag strings must be quoted
_server.Given(Request.Create().UsingGet())
.RespondWith(Response.Create().WithStatusCode(200).WithHeader("Etag", etag).WithBody(AllDataJson));
await _requestor.GetAllDataAsync();
await _requestor.GetAllDataAsync();

var reqs = new List<LogEntry>(_server.LogEntries);
Assert.Equal(2, reqs.Count);
Assert.False(reqs[0].RequestMessage.Headers.ContainsKey("If-None-Match"));
Assert.Equal(new List<string> { etag }, reqs[1].RequestMessage.Headers["If-None-Match"]);
}

[Fact]
public async Task GetAllReturnsNullIfNotModified()
{
var etag = @"""abc123"""; // note that etag strings must be quoted

_server.Given(Request.Create().UsingGet())
.RespondWith(Response.Create().WithStatusCode(200).WithHeader("Etag", etag).WithBody(AllDataJson));
var result1 = await _requestor.GetAllDataAsync();

_server.Reset();
_server.Given(Request.Create().UsingGet().WithHeader("If-None-Match", etag))
.RespondWith(Response.Create().WithStatusCode(304));
var result2 = await _requestor.GetAllDataAsync();

Assert.NotNull(result1);
Assert.Null(result2);
}

[Fact]
public async Task GetAllDoesNotRetryFailedRequest()
{
_server.Given(Request.Create().UsingGet())
.RespondWith(Response.Create().WithStatusCode(503));
try
{
await _requestor.GetAllDataAsync();
}
catch (UnsuccessfulResponseException e)
{
Assert.Equal(503, e.StatusCode);
}

var reqs = new List<LogEntry>(_server.LogEntries);
Assert.Equal(1, reqs.Count);
}

[Fact]
public async Task GetFlagUsesCorrectUriAndParsesResponseAsync()
{
var json = @"{""key"":""flag1"",""version"":1}";
_server.Given(Request.Create().UsingGet())
.RespondWith(Response.Create().WithStatusCode(200).WithBody(json));
var flag = await _requestor.GetFlagAsync("flag1");

var req = GetLastRequest();
Assert.Equal("/sdk/latest-flags/flag1", req.Path);

Assert.NotNull(flag);
Assert.Equal("flag1", flag.Key);
Assert.Equal(1, flag.Version);
}

[Fact]
public async Task GetSegmentUsesCorrectUriAndParsesResponseAsync()
{
var json = @"{""key"":""seg1"",""version"":2}";
_server.Given(Request.Create().UsingGet())
.RespondWith(Response.Create().WithStatusCode(200).WithBody(json));
var segment = await _requestor.GetSegmentAsync("seg1");

var req = GetLastRequest();
Assert.Equal("/sdk/latest-segments/seg1", req.Path);

Assert.NotNull(segment);
Assert.Equal("seg1", segment.Key);
Assert.Equal(2, segment.Version);
}

private RequestMessage GetLastRequest()
{
foreach (LogEntry le in _server.LogEntries)
{
return le.RequestMessage;
}
Assert.True(false, "Did not receive a request");
return null;
}
}
}
Loading

0 comments on commit 2d3707a

Please sign in to comment.