From a153aeac2f69e47ee8fbc9506fc35ef9bc04f1d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nuno=20G=C3=B3is?= Date: Mon, 15 Jul 2024 14:53:59 +0100 Subject: [PATCH] chore: Revert "feat: automatically generated instanceId (#226)" (#233) * chore: Revert "feat: automatically generated instanceId (#226)" * style: formatting --- README.md | 22 ++++-- samples/WebApp/Global.asax.cs | 1 + samples/WebApplication/Startup.cs | 1 + samples/WinFormsApp/Program.cs | 1 + src/Unleash/Communication/UnleashApiClient.cs | 4 +- .../UnleashApiClientRequestHeaders.cs | 2 +- src/Unleash/Events/ErrorType.cs | 7 +- src/Unleash/Internal/UnleashServices.cs | 2 +- .../Internal/UnleashSettingsValidator.cs | 3 + .../ClientRegistrationBackgroundTask.cs | 2 +- .../Scheduling/FetchFeatureTogglesTask.cs | 2 + src/Unleash/UnleashSettings.cs | 15 ++-- .../ClientFactory/SyncStartupUnitTest.cs | 2 +- .../Communication/CustomHeadersUnitTest.cs | 1 + .../UnleashApiClient_Project_Tests.cs | 1 + tests/Unleash.Tests/ExampleTests.cs | 2 +- tests/Unleash.Tests/IOTests.cs | 2 +- .../Internal/ErrorEvents_Tests.cs | 69 +++++++++++++++++++ tests/Unleash.Tests/MockedUnleashSettings.cs | 3 +- 19 files changed, 123 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 456efa68..4a306c1d 100644 --- a/README.md +++ b/README.md @@ -349,6 +349,7 @@ End Class Dim unleashSettings As New UnleashSettings() unleashSettings.AppName = "dotnet-test" +unleashSettings.InstanceTag = "instance z" ' add the custom http header provider to the settings unleashSettings.UnleashCustomHttpHeaderProvider = New CustomHttpHeaderProvider() unleashSettings.UnleashApi = new Uri("http://unleash.herokuapp.com/api/") @@ -424,13 +425,24 @@ By default unleash-client fetches the feature toggles from unleash-server every * When .json file does not exists * When the named feature toggle does not exist in .json file -The backup file name will follow this pattern: `{fileNameWithoutExtension}-{AppName}-{SdkVersion}.{extension}`. +The backup file name will follow this pattern: `{fileNameWithoutExtension}-{AppName}-{InstanceTag}-{SdkVersion}.{extension}`, where InstanceTag is either what you configure on `UnleashSettings` during startup, or a formatted string with a random component following this pattern: `{Dns.GetHostName()}-generated-{Guid.NewGuid()}`. -### InstanceTag +You can configure InstanceTag like this: -As of version `5.0.0`, `InstanceTag` is no longer a property of `UnleashSettings`. Instead, an internal `InstanceId` is automatically generated. - -This means that the backup file names have been simplified and no longer include the `InstanceTag` property value. +```csharp +var settings = new UnleashSettings() +{ + AppName = "dotnet-test", + UnleashApi = new Uri("http://unleash.herokuapp.com/api/"), + // Set an instance tag for consistent backup file naming + InstanceTag = "CustomInstanceTag", + UnleashContextProvider = new AspNetContextProvider(), + CustomHttpHeaders = new Dictionary() + { + {"Authorization", "API token" } + } +}; +``` ## Bootstrapping * Unleash supports bootstrapping from a JSON string. diff --git a/samples/WebApp/Global.asax.cs b/samples/WebApp/Global.asax.cs index e32edb9b..390b4225 100644 --- a/samples/WebApp/Global.asax.cs +++ b/samples/WebApp/Global.asax.cs @@ -27,6 +27,7 @@ protected void Application_Start() UnleashApi = new Uri("http://unleash.herokuapp.com/api/"), //UnleashApi = new Uri("http://localhost:4242/api/"), AppName = "dotnet-api-test", + InstanceTag = "instance 1", SendMetricsInterval = TimeSpan.FromSeconds(20), UnleashContextProvider = new AspNetContextProvider(), //JsonSerializer = new JsonNetSerializer() diff --git a/samples/WebApplication/Startup.cs b/samples/WebApplication/Startup.cs index 429a65d4..9b229cdd 100644 --- a/samples/WebApplication/Startup.cs +++ b/samples/WebApplication/Startup.cs @@ -26,6 +26,7 @@ public void ConfigureServices(IServiceCollection services) { UnleashApi = new Uri("http://localhost:4242/api"), AppName = "variant-sample", + InstanceTag = "instance 1", SendMetricsInterval = TimeSpan.FromSeconds(10), FetchTogglesInterval = TimeSpan.FromSeconds(10), }); diff --git a/samples/WinFormsApp/Program.cs b/samples/WinFormsApp/Program.cs index edb67b47..b2bec5df 100644 --- a/samples/WinFormsApp/Program.cs +++ b/samples/WinFormsApp/Program.cs @@ -33,6 +33,7 @@ static void Main() UnleashApi = new Uri("http://unleash.herokuapp.com/api/"), //UnleashApi = new Uri("http://localhost:4242/api/"), AppName = "dotnet-forms-test", + InstanceTag = "instance 1", SendMetricsInterval = TimeSpan.FromSeconds(5), FetchTogglesInterval = TimeSpan.FromSeconds(10), UnleashContextProvider = new WinFormsContextProvider(form), diff --git a/src/Unleash/Communication/UnleashApiClient.cs b/src/Unleash/Communication/UnleashApiClient.cs index 678b2513..184a2969 100644 --- a/src/Unleash/Communication/UnleashApiClient.cs +++ b/src/Unleash/Communication/UnleashApiClient.cs @@ -230,7 +230,7 @@ public async Task SendMetrics(ThreadSafeMetricsBucket metrics, Cancellatio jsonSerializer.Serialize(memoryStream, new ClientMetrics { AppName = clientRequestHeaders.AppName, - InstanceId = clientRequestHeaders.InstanceId, + InstanceId = clientRequestHeaders.InstanceTag, Bucket = bucket }); } @@ -290,7 +290,7 @@ private static void SetRequestHeaders(HttpRequestMessage requestMessage, Unleash requestMessage.Headers.TryAddWithoutValidation(appNameHeader, headers.AppName); requestMessage.Headers.TryAddWithoutValidation(userAgentHeader, headers.AppName); - requestMessage.Headers.TryAddWithoutValidation(instanceIdHeader, headers.InstanceId); + requestMessage.Headers.TryAddWithoutValidation(instanceIdHeader, headers.InstanceTag); requestMessage.Headers.TryAddWithoutValidation(supportedSpecVersionHeader, headers.SupportedSpecVersion); SetCustomHeaders(requestMessage, headers.CustomHttpHeaders); diff --git a/src/Unleash/Communication/UnleashApiClientRequestHeaders.cs b/src/Unleash/Communication/UnleashApiClientRequestHeaders.cs index e28d790e..5a03b8d6 100644 --- a/src/Unleash/Communication/UnleashApiClientRequestHeaders.cs +++ b/src/Unleash/Communication/UnleashApiClientRequestHeaders.cs @@ -5,7 +5,7 @@ namespace Unleash.Communication internal class UnleashApiClientRequestHeaders { public string AppName { get; set; } - public string InstanceId { get; set; } + public string InstanceTag { get; set; } public Dictionary CustomHttpHeaders { get; set; } public IUnleashCustomHttpHeaderProvider CustomHttpHeaderProvider { get; set; } public string SupportedSpecVersion { get; internal set; } diff --git a/src/Unleash/Events/ErrorType.cs b/src/Unleash/Events/ErrorType.cs index a6afefa4..193b989e 100644 --- a/src/Unleash/Events/ErrorType.cs +++ b/src/Unleash/Events/ErrorType.cs @@ -1,8 +1,13 @@ -namespace Unleash.Events +using System; +using System.Collections.Generic; +using System.Text; + +namespace Unleash.Events { public enum ErrorType { Client, + TogglesBackup, Bootstrap, ImpressionEvent, FileCache diff --git a/src/Unleash/Internal/UnleashServices.cs b/src/Unleash/Internal/UnleashServices.cs index 92576861..65d1f4da 100644 --- a/src/Unleash/Internal/UnleashServices.cs +++ b/src/Unleash/Internal/UnleashServices.cs @@ -62,7 +62,7 @@ public UnleashServices(UnleashSettings settings, EventCallbackConfig eventConfig apiClient = new UnleashApiClient(httpClient, settings.JsonSerializer, new UnleashApiClientRequestHeaders() { AppName = settings.AppName, - InstanceId = settings.InstanceId, + InstanceTag = settings.InstanceTag, CustomHttpHeaders = settings.CustomHttpHeaders, CustomHttpHeaderProvider = settings.UnleashCustomHttpHeaderProvider, SupportedSpecVersion = supportedSpecVersion diff --git a/src/Unleash/Internal/UnleashSettingsValidator.cs b/src/Unleash/Internal/UnleashSettingsValidator.cs index 020e7b3b..e6577915 100644 --- a/src/Unleash/Internal/UnleashSettingsValidator.cs +++ b/src/Unleash/Internal/UnleashSettingsValidator.cs @@ -10,6 +10,9 @@ public void Validate(UnleashSettings settings) if (settings.AppName == null) throw new UnleashException("You are required to specify an appName"); + if (settings.InstanceTag == null) + throw new UnleashException("You are required to specify an instance id"); + if (settings.JsonSerializer == null) throw new UnleashException("You are required to specify an json serializer"); diff --git a/src/Unleash/Scheduling/ClientRegistrationBackgroundTask.cs b/src/Unleash/Scheduling/ClientRegistrationBackgroundTask.cs index 97a56f8b..5bc27e52 100644 --- a/src/Unleash/Scheduling/ClientRegistrationBackgroundTask.cs +++ b/src/Unleash/Scheduling/ClientRegistrationBackgroundTask.cs @@ -34,7 +34,7 @@ public async Task ExecuteAsync(CancellationToken cancellationToken) var clientRegistration = new ClientRegistration { AppName = settings.AppName, - InstanceId = settings.InstanceId, + InstanceId = settings.InstanceTag, Interval = (long)settings.SendMetricsInterval.Value.TotalMilliseconds, SdkVersion = settings.SdkVersion, Started = DateTimeOffset.UtcNow, diff --git a/src/Unleash/Scheduling/FetchFeatureTogglesTask.cs b/src/Unleash/Scheduling/FetchFeatureTogglesTask.cs index 77b2f973..cf985c48 100644 --- a/src/Unleash/Scheduling/FetchFeatureTogglesTask.cs +++ b/src/Unleash/Scheduling/FetchFeatureTogglesTask.cs @@ -89,6 +89,7 @@ public async Task ExecuteAsync(CancellationToken cancellationToken) catch (IOException ex) { Logger.Warn(() => $"UNLEASH: Exception when writing to toggle file '{toggleFile}'.", ex); + eventConfig?.RaiseError(new ErrorEvent() { ErrorType = ErrorType.TogglesBackup, Error = ex }); } Etag = result.Etag; @@ -100,6 +101,7 @@ public async Task ExecuteAsync(CancellationToken cancellationToken) catch (IOException ex) { Logger.Warn(() => $"UNLEASH: Exception when writing to ETag file '{etagFile}'.", ex); + eventConfig?.RaiseError(new ErrorEvent() { ErrorType = ErrorType.TogglesBackup, Error = ex }); } } diff --git a/src/Unleash/UnleashSettings.cs b/src/Unleash/UnleashSettings.cs index cda8f3dd..1b2a69c0 100644 --- a/src/Unleash/UnleashSettings.cs +++ b/src/Unleash/UnleashSettings.cs @@ -46,9 +46,9 @@ public class UnleashSettings public string Environment { get; set; } = "default"; /// - /// INTERNAL: Gets or sets an instance id. Used for communication with backend api. + /// Gets or sets an instance tag. Used for communication with backend api. /// - internal string InstanceId { get; set; } = Guid.NewGuid().ToString(); + public string InstanceTag { get; set; } = GetDefaultInstanceTag(); /// /// Sets the project to fetch feature toggles for. @@ -155,6 +155,13 @@ private static string GetSdkVersion() return $"unleash-client-dotnet:v{version}"; } + private static string GetDefaultInstanceTag() + { + var hostName = Dns.GetHostName(); + + return $"{hostName}-generated-{Guid.NewGuid()}"; + } + /// /// Returns info about the unleash setup. /// @@ -164,7 +171,7 @@ public override string ToString() sb.AppendLine($"Application name: {AppName}"); sb.AppendLine($"Environment: {Environment}"); - sb.AppendLine($"Instance Id: {InstanceId}"); + sb.AppendLine($"Instance tag: {InstanceTag}"); sb.AppendLine($"Project Id: {ProjectId}"); sb.AppendLine($"Server Uri: {UnleashApi}"); sb.AppendLine($"Sdk version: {SdkVersion}"); @@ -208,7 +215,7 @@ private string PrependFileName(string filename) var extension = Path.GetExtension(filename); var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(filename); - return new string($"{fileNameWithoutExtension}-{AppName}-{SdkVersion}{extension}" + return new string($"{fileNameWithoutExtension}-{AppName}-{InstanceTag}-{SdkVersion}{extension}" .Where(c => !invalidFileNameChars.Contains(c)) .ToArray()); } diff --git a/tests/Unleash.Tests/ClientFactory/SyncStartupUnitTest.cs b/tests/Unleash.Tests/ClientFactory/SyncStartupUnitTest.cs index 66cacb00..c86cbeba 100644 --- a/tests/Unleash.Tests/ClientFactory/SyncStartupUnitTest.cs +++ b/tests/Unleash.Tests/ClientFactory/SyncStartupUnitTest.cs @@ -24,7 +24,7 @@ public class SyncStartupUnitTest public void Setup() { mockApiClient = A.Fake(); - settings = new MockedUnleashSettings(); + settings = new MockedUnleashSettings(instanceTag: "test instance SyncStartupUnitTest"); unleashFactory = new UnleashClientFactory(); } diff --git a/tests/Unleash.Tests/Communication/CustomHeadersUnitTest.cs b/tests/Unleash.Tests/Communication/CustomHeadersUnitTest.cs index 5fbede64..a390c753 100644 --- a/tests/Unleash.Tests/Communication/CustomHeadersUnitTest.cs +++ b/tests/Unleash.Tests/Communication/CustomHeadersUnitTest.cs @@ -24,6 +24,7 @@ private IUnleashApiClient CreateApiClient() var requestHeaders = new UnleashApiClientRequestHeaders { AppName = "api-test-client", + InstanceTag = "instance1", CustomHttpHeaders = httpHeaders, CustomHttpHeaderProvider = httpHeadersProvider }; diff --git a/tests/Unleash.Tests/Communication/UnleashApiClient_Project_Tests.cs b/tests/Unleash.Tests/Communication/UnleashApiClient_Project_Tests.cs index 5c960cf4..3a4954b3 100644 --- a/tests/Unleash.Tests/Communication/UnleashApiClient_Project_Tests.cs +++ b/tests/Unleash.Tests/Communication/UnleashApiClient_Project_Tests.cs @@ -27,6 +27,7 @@ private UnleashApiClient NewTestableClient(string project, MockHttpMessageHandle var requestHeaders = new UnleashApiClientRequestHeaders { AppName = "api-test-client", + InstanceTag = "instance1", CustomHttpHeaders = null, CustomHttpHeaderProvider = null }; diff --git a/tests/Unleash.Tests/ExampleTests.cs b/tests/Unleash.Tests/ExampleTests.cs index 909bf32b..ad428e71 100644 --- a/tests/Unleash.Tests/ExampleTests.cs +++ b/tests/Unleash.Tests/ExampleTests.cs @@ -14,7 +14,7 @@ public class ExampleTests public async Task Setup() { var factory = new UnleashClientFactory(); - unleash = await factory.CreateClientAsync(new MockedUnleashSettings(), true); + unleash = await factory.CreateClientAsync(new MockedUnleashSettings(instanceTag: "test instance ExampleTests"), true); } [Test] diff --git a/tests/Unleash.Tests/IOTests.cs b/tests/Unleash.Tests/IOTests.cs index 84def1ff..dff81a7a 100644 --- a/tests/Unleash.Tests/IOTests.cs +++ b/tests/Unleash.Tests/IOTests.cs @@ -24,7 +24,7 @@ private static void LockFile(object data) [Test] public async Task GracefullyFailsWhenFileLocked() { - var settings = new MockedUnleashSettings(false); + var settings = new MockedUnleashSettings(false, "test instance IOTests"); var toggleFile = settings.GetFeatureToggleFilePath(); var eTagFile = settings.GetFeatureToggleETagFilePath(); diff --git a/tests/Unleash.Tests/Internal/ErrorEvents_Tests.cs b/tests/Unleash.Tests/Internal/ErrorEvents_Tests.cs index dfb8065d..40186dab 100644 --- a/tests/Unleash.Tests/Internal/ErrorEvents_Tests.cs +++ b/tests/Unleash.Tests/Internal/ErrorEvents_Tests.cs @@ -116,6 +116,75 @@ public void FetchFeatureToggleTask_HttpRequestException_Raises_ErrorEvent() thrownException.Should().NotBeNull(); } + [Test] + public void FetchFeatureToggleTask_Serialization_Throws_Raises_ErrorEvent() + { + // Arrange + ErrorEvent callbackEvent = null; + var exceptionMessage = "Serialization failed"; + var callbackConfig = new EventCallbackConfig() + { + ErrorEvent = evt => { callbackEvent = evt; } + }; + + var fakeApiClient = A.Fake(); + A.CallTo(() => fakeApiClient.FetchToggles(A._, A._, false)) + .Returns(Task.FromResult(new FetchTogglesResult() { HasChanged = true, ToggleCollection = new ToggleCollection(), Etag = "one" })); + + var collection = new ThreadSafeToggleCollection(); + var serializer = A.Fake(); + A.CallTo(() => serializer.Serialize(A._, A._)) + .Throws(() => new IOException(exceptionMessage)); + + var filesystem = new MockFileSystem(); + var tokenSource = new CancellationTokenSource(); + var task = new FetchFeatureTogglesTask(fakeApiClient, collection, serializer, filesystem, callbackConfig, "togglefile.txt", "etagfile.txt", false); + + // Act + Task.WaitAll(task.ExecuteAsync(tokenSource.Token)); + + // Assert + callbackEvent.Should().NotBeNull(); + callbackEvent.Error.Should().NotBeNull(); + callbackEvent.Error.Message.Should().Be(exceptionMessage); + callbackEvent.ErrorType.Should().Be(ErrorType.TogglesBackup); + } + + [Test] + public void FetchFeatureToggleTask_Etag_Writing_Throws_Raises_ErrorEvent() + { + // Arrange + ErrorEvent callbackEvent = null; + var exceptionMessage = "Writing failed"; + var callbackConfig = new EventCallbackConfig() + { + ErrorEvent = evt => { callbackEvent = evt; } + }; + + var fakeApiClient = A.Fake(); + A.CallTo(() => fakeApiClient.FetchToggles(A._, A._, false)) + .Returns(Task.FromResult(new FetchTogglesResult() { HasChanged = true, ToggleCollection = new ToggleCollection(), Etag = "one" })); + + var collection = new ThreadSafeToggleCollection(); + var serializer = A.Fake(); + + var filesystem = A.Fake(); + A.CallTo(() => filesystem.WriteAllText(A._, A._)) + .Throws(() => new IOException(exceptionMessage)); + + var tokenSource = new CancellationTokenSource(); + var task = new FetchFeatureTogglesTask(fakeApiClient, collection, serializer, filesystem, callbackConfig, "togglefile.txt", "etagfile.txt", false); + + // Act + Task.WaitAll(task.ExecuteAsync(tokenSource.Token)); + + // Assert + callbackEvent.Should().NotBeNull(); + callbackEvent.Error.Should().NotBeNull(); + callbackEvent.Error.Message.Should().Be(exceptionMessage); + callbackEvent.ErrorType.Should().Be(ErrorType.TogglesBackup); + } + [Test] public void CachedFilesLoader_Raises_ErrorEvent() { diff --git a/tests/Unleash.Tests/MockedUnleashSettings.cs b/tests/Unleash.Tests/MockedUnleashSettings.cs index 145471d6..fdce4664 100644 --- a/tests/Unleash.Tests/MockedUnleashSettings.cs +++ b/tests/Unleash.Tests/MockedUnleashSettings.cs @@ -8,9 +8,10 @@ namespace Unleash.Tests { public class MockedUnleashSettings : UnleashSettings { - public MockedUnleashSettings(bool mockFileSystem = true) + public MockedUnleashSettings(bool mockFileSystem = true, string instanceTag = "test instance 1") { AppName = "test"; + InstanceTag = instanceTag; UnleashApi = new Uri("http://localhost:4242/"); DisableSingletonWarning = true;