Skip to content

Commit

Permalink
prepare 5.5.0 release (#96)
Browse files Browse the repository at this point in the history
  • Loading branch information
eli-darkly committed Oct 30, 2018
1 parent 64ba5b8 commit c678077
Show file tree
Hide file tree
Showing 24 changed files with 1,101 additions and 28 deletions.
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@

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

## [5.5.0] - 2018-10-30
### Added:
- It is now possible to inject feature flags into the client from local JSON or YAML files, replacing the normal LaunchDarkly connection. This would typically be for testing purposes. See `LaunchDarkly.Client.Files.FileComponents`.

- The `AllFlagsState` method now accepts a new option, `FlagsStateOption.DetailsOnlyForTrackedFlags`, which reduces the size of the JSON representation of the flag state by omitting some metadata. Specifically, it omits any data that is normally used for generating detailed evaluation events if a flag does not have event tracking or debugging turned on.

- The non-strong-named version of this library (`LaunchDarkly.Common`) can now be used with a non-strong-named version of LaunchDarkly.Client, which does not normally exist but could be built as part of a fork of the SDK.

### Changed:
- Previously, the delay before stream reconnect attempts would increase exponentially only if the previous connection could not be made at all or returned an HTTP error; if it received an HTTP 200 status, the delay would be reset to the minimum even if the connection then immediately failed. Now, if the stream connection fails after it has been up for less than a minute, the reconnect delay will continue to increase.

### Fixed:
- Fixed an [unobserved exception](https://blogs.msdn.microsoft.com/pfxteam/2011/09/28/task-exception-handling-in-net-4-5/) that could occur following a stream timeout, which could cause a crash in .NET 4.0.

- Fixed a `NullReferenceException` that could sometimes appear in the log if a stream connection failed.

- Fixed the documentation for `Configuration.StartWaitTime` to indicate that the default is 10 seconds, not 5 seconds. (Thanks, [KimboTodd](https://github.com/launchdarkly/dotnet-client/pull/95)!)

- JSON data from `AllFlagsState` is now slightly smaller even if you do not use the new option described above, because it completely omits the flag property for event tracking unless that property is true.

## [5.4.0] - 2018-08-30
### Added:
- The new `LDClient` methods `BoolVariationDetail`, `IntVariationDetail`, `DoubleVariationDetail`, `StringVariationDetail`, and `JsonVariationDetail` allow you to evaluate a feature flag (using the same parameters as you would for `BoolVariation`, etc.) and receive more information about how the value was calculated. This information is returned in an `EvaluationDetail` object, which contains both the result value and an `EvaluationReason` which will tell you, for instance, if the user was individually targeted for the flag or was matched by one of the flag's rules, or if the flag returned the default value due to an error.
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ Redis integration

An implementation of feature flag caching using a Redis server is available as a separate package. More information and source code [here](https://github.com/launchdarkly/dotnet-client-redis).

Using flag data from a file
---------------------------
For testing purposes, the SDK can be made to read feature flag state from a file or files instead of connecting to LaunchDarkly. See `LaunchDarkly.Client.Files.FileComponents` for more details.

Learn more
-----------

Expand Down
28 changes: 18 additions & 10 deletions src/LaunchDarkly.Client/FeatureFlagsState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,25 @@ internal FeatureFlagsState(bool valid, IDictionary<string, JToken> values,
_flagMetadata = metadata;
}

internal void AddFlag(FeatureFlag flag, JToken value, int? variation, EvaluationReason reason)
internal void AddFlag(FeatureFlag flag, JToken value, int? variation, EvaluationReason reason,
bool detailsOnlyIfTracked)
{
_flagValues[flag.Key] = value;
_flagMetadata[flag.Key] = new FlagMetadata
var meta = new FlagMetadata
{
Variation = variation,
Version = flag.Version,
TrackEvents = flag.TrackEvents,
DebugEventsUntilDate = flag.DebugEventsUntilDate,
Reason = reason
DebugEventsUntilDate = flag.DebugEventsUntilDate
};
if (!detailsOnlyIfTracked || flag.TrackEvents || flag.DebugEventsUntilDate != null)
{
meta.Version = flag.Version;
meta.Reason = reason;
}
if (flag.TrackEvents)
{
meta.TrackEvents = true;
}
_flagMetadata[flag.Key] = meta;
}

/// <summary>
Expand Down Expand Up @@ -123,10 +131,10 @@ internal class FlagMetadata
{
[JsonProperty(PropertyName = "variation", NullValueHandling = NullValueHandling.Ignore)]
internal int? Variation { get; set; }
[JsonProperty(PropertyName = "version")]
internal int Version { get; set; }
[JsonProperty(PropertyName = "trackEvents")]
internal bool TrackEvents { get; set; }
[JsonProperty(PropertyName = "version", NullValueHandling = NullValueHandling.Ignore)]
internal int? Version { get; set; }
[JsonProperty(PropertyName = "trackEvents", NullValueHandling = NullValueHandling.Ignore)]
internal bool? TrackEvents { get; set; }
[JsonProperty(PropertyName = "debugEventsUntilDate", NullValueHandling = NullValueHandling.Ignore)]
internal long? DebugEventsUntilDate { get; set; }
[JsonProperty(PropertyName = "reason", NullValueHandling = NullValueHandling.Ignore)]
Expand Down
107 changes: 107 additions & 0 deletions src/LaunchDarkly.Client/Files/FileComponents.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
namespace LaunchDarkly.Client.Files
{
/// <summary>
/// The entry point for the file data source, which allows you to use local files as a source of
/// feature flag state.
/// </summary>
/// <remarks>
/// <para>
/// This would typically be used in a test environment, to operate using a predetermined feature flag
/// state without an actual LaunchDarkly connection.
/// </para>
/// <para>
/// To use this component, call <see cref="FileDataSource()"/> to obtain a factory object, call one or
/// methods to configure it, and then add it to your LaunchDarkly client configuration. At a
/// minimum, you will want to call <see cref="FileDataSourceFactory.WithFilePaths(string[])"/> to specify
/// your data file(s); you can also use <see cref="FileDataSourceFactory.WithAutoUpdate(bool)"/> to
/// specify that flags should be reloaded when a file is modified. See <see cref="FileDataSourceFactory"/>
/// for all configuration options.
/// </para>
/// <code>
/// var fileSource = FileComponents.FileDataSource()
/// .WithFilePaths("./testData/flags.json")
/// .WithAutoUpdate(true);
/// var config = Configuration.Default("sdkKey")
/// .WithUpdateProcessorFactory(fileSource)
/// .Build();
/// </code>
/// <para>
/// This will cause the client <i>not</i> to connect to LaunchDarkly to get feature flags. The
/// client may still make network connections to send analytics events, unless you have disabled
/// this with <c>configuration.WithEventProcessor(Components.NullEventProcessor)</c>.
/// </para>
/// <para>
/// Flag data files are JSON by default (although it is possible to specify a parser for another format,
/// such as YAML; see <see cref="FileDataSourceFactory.WithParser(System.Func{string, object})"/>). They
/// contain an object with three possible properties:
/// </para>
/// <list type="bullet">
/// <item><c>flags</c>: Feature flag definitions.</item>
/// <item><c>flagVersions</c>: Simplified feature flags that contain only a value.</item>
/// <item><c>segments</c>: User segment definitions.</item>
/// </list>
/// <para>
/// The format of the data in <c>flags</c> and <c>segments</c> is defined by the LaunchDarkly application
/// and is subject to change. Rather than trying to construct these objects yourself, it is simpler
/// to request existing flags directly from the LaunchDarkly server in JSON format, and use this
/// output as the starting point for your file. In Linux you would do this:
/// </para>
/// <code>
/// curl -H "Authorization: {your sdk key}" https://app.launchdarkly.com/sdk/latest-all
/// </code>
/// <para>
/// The output will look something like this (but with many more properties):
/// </para>
/// <code>
/// {
/// "flags": {
/// "flag-key-1": {
/// "key": "flag-key-1",
/// "on": true,
/// "variations": [ "a", "b" ]
/// }
/// },
/// "segments": {
/// "segment-key-1": {
/// "key": "segment-key-1",
/// "includes": [ "user-key-1" ]
/// }
/// }
/// }
/// </code>
/// <para>
/// Data in this format allows the SDK to exactly duplicate all the kinds of flag behavior supported
/// by LaunchDarkly. However, in many cases you will not need this complexity, but will just want to
/// set specific flag keys to specific values. For that, you can use a much simpler format:
/// </para>
/// <code>
/// {
/// "flagValues": {
/// "my-string-flag-key": "value-1",
/// "my-boolean-flag-key": true,
/// "my-integer-flag-key": 3
/// }
/// }
/// </code>
/// <para>
/// It is also possible to specify both <c>flags</c> and <c>flagValues</c>, if you want some flags
/// to have simple values and others to have complex behavior. However, it is an error to use the
/// same flag key or segment key more than once, either in a single file or across multiple files.
/// </para>
/// <para>
/// If the data source encounters any error in any file-- malformed content, a missing file, or a
/// duplicate key-- it will not load flags from any of the files.
/// </para>
/// </remarks>
public static class FileComponents
{
/// <summary>
/// Creates a <see cref="FileDataSourceFactory"/> which you can use to configure the file data source.
/// </summary>
/// <returns>a <see cref="FileDataSourceFactory"/></returns>
public static FileDataSourceFactory FileDataSource()
{
return new FileDataSourceFactory();
}
}
}
109 changes: 109 additions & 0 deletions src/LaunchDarkly.Client/Files/FileDataSource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Common.Logging;

namespace LaunchDarkly.Client.Files
{
internal class FileDataSource : IUpdateProcessor
{
private static readonly ILog Log = LogManager.GetLogger(typeof(FileDataSource));
private readonly IFeatureStore _featureStore;
private readonly List<string> _paths;
private readonly IDisposable _reloader;
private readonly FlagFileParser _parser;
private volatile bool _started;
private volatile bool _loadedValidData;

public FileDataSource(IFeatureStore featureStore, List<string> paths, bool autoUpdate, TimeSpan pollInterval,
Func<string, object> alternateParser)
{
_featureStore = featureStore;
_paths = new List<string>(paths);
_parser = new FlagFileParser(alternateParser);
if (autoUpdate)
{
try
{
#if NETSTANDARD1_4 || NETSTANDARD1_6
_reloader = new FilePollingReloader(_paths, TriggerReload, pollInterval);
#else
_reloader = new FileWatchingReloader(_paths, TriggerReload);
#endif
}
catch (Exception e)
{
Log.ErrorFormat("Unable to watch files for auto-updating: {0}", e);
_reloader = null;
}
}
else
{
_reloader = null;
}
}

public Task<bool> Start()
{
_started = true;
LoadAll();

// We always complete the start task regardless of whether we successfully loaded data or not;
// if the data files were bad, they're unlikely to become good within the short interval that
// LdClient waits on this task, even if auto-updating is on.
TaskCompletionSource<bool> initTask = new TaskCompletionSource<bool>();
initTask.SetResult(_loadedValidData);
return initTask.Task;
}

public bool Initialized()
{
return _loadedValidData;
}

public void Dispose()
{
Dispose(true);
}

private void Dispose(bool disposing)
{
if (disposing)
{
_reloader?.Dispose();
}
}

private void LoadAll()
{
Dictionary<IVersionedDataKind, IDictionary<string, IVersionedData>> allData =
new Dictionary<IVersionedDataKind, IDictionary<string, IVersionedData>>();
foreach (var path in _paths)
{
try
{
var content = File.ReadAllText(path);
var data = _parser.Parse(content);
data.AddToData(allData);
}
catch (Exception e)
{
Log.ErrorFormat("{0}: {1}", path, e);
return;
}
}
_featureStore.Init(allData);
_loadedValidData = true;
}

private void TriggerReload()
{
if (_started)
{
LoadAll();
}
}
}
}
Loading

0 comments on commit c678077

Please sign in to comment.