From 821ac3e77e9d585afd8be5618e92dd57994a2e1a Mon Sep 17 00:00:00 2001 From: eran Date: Wed, 22 Aug 2018 14:07:15 +0300 Subject: [PATCH 1/4] Feature/AddDefaultRegexTimeout *Add default regex timeout (10 seconds) --- .paket/Paket.Restore.targets | 53 ++++++------------- .../MicrodotServiceHost.cs | 2 +- .../Gigya.Microdot.Ninject.csproj | 1 + Gigya.Microdot.Ninject/MicrodotModule.cs | 4 ++ .../RegexTimeoutInitializer.cs | 27 ++++++++++ .../MicrodotOrleansServiceHost.cs | 3 -- .../CalculatorServiceTests.cs | 7 +++ .../CalculatorServiceGrain.cs | 8 +++ .../CalculatorService/ICalculatorService.cs | 2 +- 9 files changed, 64 insertions(+), 43 deletions(-) create mode 100644 Gigya.Microdot.Ninject/RegexTimeoutInitializer.cs diff --git a/.paket/Paket.Restore.targets b/.paket/Paket.Restore.targets index e12083c1..e7c1bc0c 100644 --- a/.paket/Paket.Restore.targets +++ b/.paket/Paket.Restore.targets @@ -43,26 +43,23 @@ true - $(NoWarn);NU1603;NU1604;NU1605;NU1608 + $(NoWarn);NU1603 - /usr/bin/shasum "$(PaketRestoreCacheFile)" | /usr/bin/awk '{ print $1 }' - /usr/bin/shasum "$(PaketLockFilePath)" | /usr/bin/awk '{ print $1 }' + /usr/bin/shasum $(PaketRestoreCacheFile) | /usr/bin/awk '{ print $1 }' + /usr/bin/shasum $(PaketLockFilePath) | /usr/bin/awk '{ print $1 }' - + - + - - - $([System.IO.File]::ReadAllText('$(PaketRestoreCacheFile)')) @@ -71,23 +68,12 @@ false true - - - true - - - - - - - - $(MSBuildProjectDirectory)\obj\$(MSBuildProjectFile).paket.references.cached @@ -96,9 +82,7 @@ $(MSBuildProjectDirectory)\$(MSBuildProjectName).paket.references $(MSBuildProjectDirectory)\paket.references - - false - true + $(MSBuildProjectDirectory)\obj\$(MSBuildProjectFile).$(TargetFramework).paket.resolved true references-file-or-cache-not-found @@ -117,29 +101,24 @@ - + true - target-framework '$(TargetFramework)' or '$(TargetFrameworks)' files @(PaketResolvedFilePaths) + target-framework '$(TargetFramework)' - - + - - false - true - - + - + - + $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[0]) $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[1]) @@ -147,9 +126,7 @@ %(PaketReferencesFileLinesInfo.PackageVersion) - All - runtime - true + All @@ -206,8 +183,8 @@ - - + + (); GetLoggingModule().Bind(kernel.Rebind(), kernel.Rebind()); kernel.Rebind().ToConstant(Arguments); + } /// diff --git a/Gigya.Microdot.Ninject/Gigya.Microdot.Ninject.csproj b/Gigya.Microdot.Ninject/Gigya.Microdot.Ninject.csproj index 267036c9..10b4e8a9 100644 --- a/Gigya.Microdot.Ninject/Gigya.Microdot.Ninject.csproj +++ b/Gigya.Microdot.Ninject/Gigya.Microdot.Ninject.csproj @@ -51,6 +51,7 @@ + diff --git a/Gigya.Microdot.Ninject/MicrodotModule.cs b/Gigya.Microdot.Ninject/MicrodotModule.cs index 2d5facee..2b2567c1 100644 --- a/Gigya.Microdot.Ninject/MicrodotModule.cs +++ b/Gigya.Microdot.Ninject/MicrodotModule.cs @@ -42,6 +42,7 @@ namespace Gigya.Microdot.Ninject /// public class MicrodotModule : NinjectModule { + private readonly Type[] NonSingletonBaseTypes = { typeof(ConsulDiscoverySource), @@ -51,6 +52,9 @@ public class MicrodotModule : NinjectModule public override void Load() { + //Need to be initialized before using any regex! + new RegexTimeoutInitializer().Init(); + Kernel .Bind(typeof(ConcurrentDictionary<,>)) .To(typeof(DisposableConcurrentDictionary<,>)) diff --git a/Gigya.Microdot.Ninject/RegexTimeoutInitializer.cs b/Gigya.Microdot.Ninject/RegexTimeoutInitializer.cs new file mode 100644 index 00000000..00b56631 --- /dev/null +++ b/Gigya.Microdot.Ninject/RegexTimeoutInitializer.cs @@ -0,0 +1,27 @@ +using System; +using System.Configuration; + +namespace Gigya.Microdot.Ninject +{ + + /// + /// Notice that REGEX_DEFAULT_MATCH_TIMEOUT can be set only once and will be determined when calling the first regex the default in infinite!! + /// + public class RegexTimeoutInitializer + { + public void Init() + { + int regexDefaultMachTimeOutMs =(int) TimeSpan.FromSeconds(10).TotalMilliseconds; + try + { + regexDefaultMachTimeOutMs = int.Parse(ConfigurationManager.AppSettings["regexDefaultMachTimeOutMs"]); + } + catch (Exception e) + { + } + + AppDomain.CurrentDomain.SetData("REGEX_DEFAULT_MATCH_TIMEOUT",TimeSpan.FromMilliseconds(regexDefaultMachTimeOutMs)); + Console.WriteLine($"REGEX_DEFAULT_MATCH_TIMEOUT is set to {regexDefaultMachTimeOutMs} ms"); + } + } +} \ No newline at end of file diff --git a/Gigya.Microdot.Orleans.Ninject.Host/MicrodotOrleansServiceHost.cs b/Gigya.Microdot.Orleans.Ninject.Host/MicrodotOrleansServiceHost.cs index 30e8ccdb..fd4d579d 100644 --- a/Gigya.Microdot.Orleans.Ninject.Host/MicrodotOrleansServiceHost.cs +++ b/Gigya.Microdot.Orleans.Ninject.Host/MicrodotOrleansServiceHost.cs @@ -89,7 +89,6 @@ protected override void OnStart() protected virtual void PreInitialize(IKernel kernel) { kernel.Get().Validate(); - CrashHandler = kernel.Get>()(OnCrash); var metricsInitializer = kernel.Get(); metricsInitializer.Init(); @@ -129,9 +128,7 @@ protected virtual void PreConfigure(IKernel kernel) kernel.Load(); kernel.Load(); kernel.Load(); - kernel.Rebind().ToConstant(Arguments); - GetLoggingModule().Bind(kernel.Rebind(), kernel.Rebind()); } diff --git a/tests/Gigya.Microdot.Orleans.Hosting.UnitTests/CalculatorServiceTests.cs b/tests/Gigya.Microdot.Orleans.Hosting.UnitTests/CalculatorServiceTests.cs index 3f61c029..8eb8a5a2 100644 --- a/tests/Gigya.Microdot.Orleans.Hosting.UnitTests/CalculatorServiceTests.cs +++ b/tests/Gigya.Microdot.Orleans.Hosting.UnitTests/CalculatorServiceTests.cs @@ -24,6 +24,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Gigya.Microdot.Fakes; using Gigya.Microdot.Interfaces; @@ -381,6 +382,12 @@ public async Task LogGrainId() await Service.LogGrainId(); } + [Test] + public async Task RegexTestWithTimeout() + { + await Service.RegexTestWithDefaultTimeoutDefault( 10); + } + #region MockData public class Person { diff --git a/tests/Gigya.Microdot.Orleans.Hosting.UnitTests/Microservice/CalculatorService/CalculatorServiceGrain.cs b/tests/Gigya.Microdot.Orleans.Hosting.UnitTests/Microservice/CalculatorService/CalculatorServiceGrain.cs index d4210f3a..72f68459 100644 --- a/tests/Gigya.Microdot.Orleans.Hosting.UnitTests/Microservice/CalculatorService/CalculatorServiceGrain.cs +++ b/tests/Gigya.Microdot.Orleans.Hosting.UnitTests/Microservice/CalculatorService/CalculatorServiceGrain.cs @@ -22,7 +22,9 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Gigya.Microdot.Fakes; using Gigya.Microdot.Hosting.Events; @@ -237,6 +239,12 @@ public async Task ValidatePersonLogFields([LogFields]CalculatorServiceTest return true; } + public async Task RegexTestWithDefaultTimeoutDefault(int defaultTimeoutInSeconds) + { + var regex = new Regex("a"); + regex.MatchTimeout.ShouldBe(TimeSpan.FromSeconds(defaultTimeoutInSeconds)); + } + private string AddPrifix(string prefix, string param) { return $"{prefix.Substring(0, 1).ToLower()}{prefix.Substring(1)}_{param}"; diff --git a/tests/Gigya.Microdot.Orleans.Hosting.UnitTests/Microservice/CalculatorService/ICalculatorService.cs b/tests/Gigya.Microdot.Orleans.Hosting.UnitTests/Microservice/CalculatorService/ICalculatorService.cs index b153c22c..991ca318 100644 --- a/tests/Gigya.Microdot.Orleans.Hosting.UnitTests/Microservice/CalculatorService/ICalculatorService.cs +++ b/tests/Gigya.Microdot.Orleans.Hosting.UnitTests/Microservice/CalculatorService/ICalculatorService.cs @@ -65,6 +65,6 @@ public interface ICalculatorService Task CreatePerson([LogFields] CalculatorServiceTests.Person person); Task LogGrainId(); Task ValidatePersonLogFields([LogFields] CalculatorServiceTests.Person person); - + Task RegexTestWithDefaultTimeoutDefault(int defaultTimeoutInSeconds); } } \ No newline at end of file From b0b24b0c2f7afb7dba4c6e7dd314243f25249505 Mon Sep 17 00:00:00 2001 From: "eran.of" Date: Tue, 2 Oct 2018 20:29:27 +0300 Subject: [PATCH 2/4] set ninject ActivationCacheDisabled to be default ture --- Gigya.Microdot.Ninject.Host/MicrodotServiceHost.cs | 6 +++--- .../MicrodotOrleansServiceHost.cs | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Gigya.Microdot.Ninject.Host/MicrodotServiceHost.cs b/Gigya.Microdot.Ninject.Host/MicrodotServiceHost.cs index 0ec393a7..7b57d90d 100644 --- a/Gigya.Microdot.Ninject.Host/MicrodotServiceHost.cs +++ b/Gigya.Microdot.Ninject.Host/MicrodotServiceHost.cs @@ -117,7 +117,7 @@ protected virtual void OnInitilize(IResolutionRoot resolutionRoot) /// The kernel to use. protected virtual IKernel CreateKernel() { - return new StandardKernel(); + return new StandardKernel(new NinjectSettings { ActivationCacheDisabled = true }); } @@ -154,11 +154,11 @@ protected virtual void PreConfigure(IKernel kernel) /// method. /// protected override void OnStop() - { + { if (Arguments.ServiceDrainTimeSec.HasValue) { Kernel.Get().StartDrain(); - Thread.Sleep(Arguments.ServiceDrainTimeSec.Value * 1000 ); + Thread.Sleep(Arguments.ServiceDrainTimeSec.Value * 1000); } Dispose(); } diff --git a/Gigya.Microdot.Orleans.Ninject.Host/MicrodotOrleansServiceHost.cs b/Gigya.Microdot.Orleans.Ninject.Host/MicrodotOrleansServiceHost.cs index fd4d579d..c887d859 100644 --- a/Gigya.Microdot.Orleans.Ninject.Host/MicrodotOrleansServiceHost.cs +++ b/Gigya.Microdot.Orleans.Ninject.Host/MicrodotOrleansServiceHost.cs @@ -111,7 +111,8 @@ protected virtual void OnInitilize(IResolutionRoot resolutionRoot) /// The kernel to use. protected virtual IKernel CreateKernel() { - return new StandardKernel(); + return new StandardKernel(new NinjectSettings { ActivationCacheDisabled = true }); + } From c5c503a18ccc0b5eb48f25e35a7b371b6803f2d2 Mon Sep 17 00:00:00 2001 From: eran Date: Sun, 7 Oct 2018 14:29:36 +0300 Subject: [PATCH 3/4] Feature/discovery rewrite (#207) Discovery Rewrite and Availability Zone --- .../ConfigurationLocationsParser.cs | 8 +- .../EnvironmentVariableProvider.cs | 79 +++- .../EnvironmentVariablesFileReader.cs | 16 +- Gigya.Microdot.Fakes/DateTimeFake.cs | 27 +- .../Discovery/AlwaysLocalHost.cs | 4 +- .../Discovery/AlwaysLocalHostDiscovery.cs | 49 +-- .../Discovery/LocalhostServiceDiscovery.cs | 7 +- .../Gigya.Microdot.Fakes.csproj | 1 + .../Events/ServiceCallEvent.cs | 2 +- .../Endpoints/ConfigurationResponseBuilder.cs | 2 +- .../HttpService/Endpoints/SchemaEndpoint.cs | 9 +- .../HttpService/HttpServiceListener.cs | 33 +- .../HttpService/IServiceEndPointDefinition.cs | 2 +- .../HttpService/ServerRequestPublisher.cs | 3 +- .../HttpService/ServiceEndPointDefinition.cs | 4 +- .../HttpService/ServiceMethodResolver.cs | 2 +- .../IEnvironmentVariableProvider.cs | 14 +- Gigya.Microdot.Interfaces/Events/IEvent.cs | 3 +- .../Gigya.Microdot.Interfaces.csproj | 4 - .../SystemWrappers/IDateTime.cs | 6 + .../SystemWrappers/IEnvironment.cs | 25 +- Gigya.Microdot.Ninject/MicrodotModule.cs | 25 +- .../ClusterIdentity.cs | 7 +- .../Config/ConsulConfig.cs | 16 +- .../Config/DiscoveryConfig.cs | 8 + .../Config/ServiceDiscoveryConfig.cs | 3 + .../Config/ServiceScope.cs | 13 +- .../ConfigDiscoverySource.cs | 2 +- .../ConsulClient.cs | 172 ++------ .../ConsulContracts.cs | 54 +++ .../ConsulDiscoverySource.cs | 14 +- .../DeploymentIdentifier.cs | 100 +++++ .../DiscoverySourceLoader.cs | 10 +- .../Gigya.Microdot.ServiceDiscovery.csproj | 46 ++- .../HostManagement/RemoteHost.cs | 16 +- .../HostManagement/RemoteHostPool.cs | 23 +- .../IServiceDiscovery.cs | 2 +- .../LocalDiscoverySource.cs | 2 +- .../{IEndPointHandle.cs => MonitoredNode.cs} | 0 .../Properties/AssemblyInfo.cs | 3 +- .../Rewrite/ConfigNodeSource.cs | 131 ++++++ .../Rewrite/ConsulClient.cs | 269 +++++++++++++ .../Rewrite/ConsulNode.cs | 40 ++ .../Rewrite/ConsulNodeSource.cs | 242 +++++++++++ .../Rewrite/ConsulNodeSourceFactory.cs | 177 ++++++++ .../Rewrite/ConsulResponse.cs | 93 +++++ .../DeploymentIdentifierConsulExtensions.cs | 37 ++ .../Rewrite/Discovery.cs | 204 ++++++++++ Gigya.Microdot.ServiceDiscovery/Rewrite/Ex.cs | 41 ++ .../Rewrite/IConsulClient.cs | 66 +++ .../Rewrite/IConsulServiceListMonitor.cs | 14 + .../Rewrite/IDiscovery.cs | 52 +++ .../Rewrite/ILoadBalancer.cs | 46 +++ .../Rewrite/INewServiceDiscovery.cs | 35 ++ .../Rewrite/INodeSource.cs | 41 ++ .../Rewrite/INodeSourceFactory.cs | 51 +++ .../Rewrite/LoadBalancer.cs | 245 +++++++++++ .../Rewrite/LocalNodeSource.cs | 48 +++ .../Rewrite/NewServiceDiscovery.cs | 179 ++++++++ .../Rewrite/NodeMonitoringState.cs | 147 +++++++ .../Rewrite/ReachabilityCheck.cs | 34 ++ .../Rewrite/TrafficRoutingStrategy.cs | 42 ++ .../Rewrite/_diagram.png | Bin 0 -> 202849 bytes .../Rewrite/_diagram_from_draw.io.xml | 1 + .../ServiceDiscovery.cs | 23 +- .../paket.references | 3 +- .../Caching/AsyncMemoizer.cs | 11 +- .../Caching/IMemoizer.cs | 2 +- .../Gigya.Microdot.ServiceProxy.csproj | 4 + .../IServiceProxyProvider.cs | 2 +- .../Rewrite/IMemoizer.cs | 35 ++ .../Rewrite/IProxyable.cs | 42 ++ .../Rewrite/IServiceProxyProvider.cs | 58 +++ .../Rewrite/ServiceProxyProvider.cs | 93 +++++ .../ServiceProxyExtensions.cs | 21 +- .../ServiceProxyProvider.cs | 87 ++-- .../ServiceProxyProviderGeneric.cs | 2 +- Gigya.Microdot.SharedLogic/Events/Event.cs | 20 +- .../Events/EventConsts.cs | 2 + .../Events/EventSerializer.cs | 9 +- .../Events/TracingContext.cs | 10 +- .../Exceptions/StackTraceEnhancer.cs | 11 +- .../Gigya.Microdot.SharedLogic.csproj | 17 + .../GigyaHttpHeaders.cs | 3 +- .../HttpService/HttpServiceRequest.cs | 67 ++- .../HttpService/ICertificateLocator.cs | 2 +- .../HttpService/RequestOverrides.cs | 7 +- .../HttpService/ServiceReachabilityStatus.cs | 2 +- .../Monitor/AggregatingHealthStatus.cs | 58 ++- Gigya.Microdot.SharedLogic/Rewrite/Node.cs | 36 ++ .../WindowsStoreCertificateLocator.cs | 2 +- .../SystemWrappers/DateTimeImpl.cs | 18 +- .../SystemWrappers/EnvironmentInstance.cs | 38 +- .../Utils/Extensions.cs | 17 + Gigya.Microdot.SharedLogic/paket.references | 3 +- .../Service/ServiceTesterBase.cs | 1 + .../TestingKernel.cs | 4 + .../Attributes/HttpServiceAttribute.cs | 9 +- Sample/CalculatorService.Client/Program.cs | 3 +- .../CalculatorServiceHost.cs | 3 +- .../CalculatorServiceHost.cs | 3 +- paket.dependencies | 3 + .../MicroServiceTests.cs | 9 + .../AssemblyInitialize.cs | 3 +- .../CalculatorServiceTests.cs | 4 +- ....Microdot.Orleans.Hosting.UnitTests.csproj | 1 + .../ServiceSchemaTests.cs | 72 ++++ .../ServiceSchemaTests.cs | 3 +- .../AssemblyInitialize.cs | 6 +- .../Caching/CachingProxyTests.cs | 2 +- .../Caching/Host/SlowServiceHost.cs | 2 + .../EnviromentVariablesFileReaderTests.cs | 129 ------ .../EnvironmentVariableProviderTests.cs | 128 ++++++ .../Configuration/MasterConfigParserTests.cs | 32 +- .../Discovery/ConfigNodeSourceTests.cs | 132 ++++++ .../Discovery/ConsulClientTests.cs | 26 +- .../ConsulDiscoveryMasterFallBackTest.cs | 26 +- .../Discovery/ConsulDiscoverySourceTest.cs | 17 +- .../Discovery/ConsulSimulator.cs | 42 +- .../Discovery/LocalNodeSourceTests.cs | 51 +++ .../Discovery/RemoteHostPoolTests.cs | 5 +- .../Rewrite/ConsulNodeSourceFactoryTests.cs | 221 ++++++++++ .../Rewrite/ConsulNodeSourceTests.cs | 285 +++++++++++++ .../Discovery/Rewrite/DiscoveryTests.cs | 295 ++++++++++++++ .../Discovery/Rewrite/LoadBalancerTests.cs | 381 ++++++++++++++++++ .../NewConsulDiscoveryMasterFallBackTest.cs | 251 ++++++++++++ .../NewServiceDiscoveryConfigChangeTest.cs | 81 ++++ .../ServiceDiscoveryConfigChangeTest.cs | 6 +- .../Events/EventSerializationTests.cs | 18 +- .../Gigya.Microdot.UnitTests.csproj | 12 +- .../HttpServiceRequestTests.cs | 4 +- .../Gigya.Microdot.UnitTests/IDemoService.cs | 4 +- .../HttpServiceListenerTests.cs | 17 +- .../PortsAllocationTests.cs | 4 +- .../AbstractServiceProxyTest.cs | 3 +- .../ServiceProxyTests/BehaviorTests.cs | 32 +- .../ServiceProxyTests/MetricsTests.cs | 2 +- 137 files changed, 5406 insertions(+), 682 deletions(-) rename Gigya.Microdot.ServiceDiscovery/ServiceDeployment.cs => Gigya.Microdot.Fakes/Discovery/AlwaysLocalHostDiscovery.cs (51%) create mode 100644 Gigya.Microdot.ServiceDiscovery/ConsulContracts.cs create mode 100644 Gigya.Microdot.ServiceDiscovery/DeploymentIdentifier.cs rename Gigya.Microdot.ServiceDiscovery/{IEndPointHandle.cs => MonitoredNode.cs} (100%) create mode 100644 Gigya.Microdot.ServiceDiscovery/Rewrite/ConfigNodeSource.cs create mode 100644 Gigya.Microdot.ServiceDiscovery/Rewrite/ConsulClient.cs create mode 100644 Gigya.Microdot.ServiceDiscovery/Rewrite/ConsulNode.cs create mode 100644 Gigya.Microdot.ServiceDiscovery/Rewrite/ConsulNodeSource.cs create mode 100644 Gigya.Microdot.ServiceDiscovery/Rewrite/ConsulNodeSourceFactory.cs create mode 100644 Gigya.Microdot.ServiceDiscovery/Rewrite/ConsulResponse.cs create mode 100644 Gigya.Microdot.ServiceDiscovery/Rewrite/DeploymentIdentifierConsulExtensions.cs create mode 100644 Gigya.Microdot.ServiceDiscovery/Rewrite/Discovery.cs create mode 100644 Gigya.Microdot.ServiceDiscovery/Rewrite/Ex.cs create mode 100644 Gigya.Microdot.ServiceDiscovery/Rewrite/IConsulClient.cs create mode 100644 Gigya.Microdot.ServiceDiscovery/Rewrite/IConsulServiceListMonitor.cs create mode 100644 Gigya.Microdot.ServiceDiscovery/Rewrite/IDiscovery.cs create mode 100644 Gigya.Microdot.ServiceDiscovery/Rewrite/ILoadBalancer.cs create mode 100644 Gigya.Microdot.ServiceDiscovery/Rewrite/INewServiceDiscovery.cs create mode 100644 Gigya.Microdot.ServiceDiscovery/Rewrite/INodeSource.cs create mode 100644 Gigya.Microdot.ServiceDiscovery/Rewrite/INodeSourceFactory.cs create mode 100644 Gigya.Microdot.ServiceDiscovery/Rewrite/LoadBalancer.cs create mode 100644 Gigya.Microdot.ServiceDiscovery/Rewrite/LocalNodeSource.cs create mode 100644 Gigya.Microdot.ServiceDiscovery/Rewrite/NewServiceDiscovery.cs create mode 100644 Gigya.Microdot.ServiceDiscovery/Rewrite/NodeMonitoringState.cs create mode 100644 Gigya.Microdot.ServiceDiscovery/Rewrite/ReachabilityCheck.cs create mode 100644 Gigya.Microdot.ServiceDiscovery/Rewrite/TrafficRoutingStrategy.cs create mode 100644 Gigya.Microdot.ServiceDiscovery/Rewrite/_diagram.png create mode 100644 Gigya.Microdot.ServiceDiscovery/Rewrite/_diagram_from_draw.io.xml create mode 100644 Gigya.Microdot.ServiceProxy/Rewrite/IMemoizer.cs create mode 100644 Gigya.Microdot.ServiceProxy/Rewrite/IProxyable.cs create mode 100644 Gigya.Microdot.ServiceProxy/Rewrite/IServiceProxyProvider.cs create mode 100644 Gigya.Microdot.ServiceProxy/Rewrite/ServiceProxyProvider.cs rename {Gigya.Microdot.Interfaces => Gigya.Microdot.SharedLogic}/HttpService/HttpServiceRequest.cs (79%) rename {Gigya.Microdot.Interfaces => Gigya.Microdot.SharedLogic}/HttpService/ICertificateLocator.cs (96%) rename {Gigya.Microdot.Interfaces => Gigya.Microdot.SharedLogic}/HttpService/RequestOverrides.cs (90%) rename {Gigya.Microdot.Interfaces => Gigya.Microdot.SharedLogic}/HttpService/ServiceReachabilityStatus.cs (96%) create mode 100644 Gigya.Microdot.SharedLogic/Rewrite/Node.cs create mode 100644 tests/Gigya.Microdot.Orleans.Hosting.UnitTests/ServiceSchemaTests.cs delete mode 100644 tests/Gigya.Microdot.UnitTests/Configuration/EnviromentVariablesFileReaderTests.cs create mode 100644 tests/Gigya.Microdot.UnitTests/Configuration/EnvironmentVariableProviderTests.cs create mode 100644 tests/Gigya.Microdot.UnitTests/Discovery/ConfigNodeSourceTests.cs create mode 100644 tests/Gigya.Microdot.UnitTests/Discovery/LocalNodeSourceTests.cs create mode 100644 tests/Gigya.Microdot.UnitTests/Discovery/Rewrite/ConsulNodeSourceFactoryTests.cs create mode 100644 tests/Gigya.Microdot.UnitTests/Discovery/Rewrite/ConsulNodeSourceTests.cs create mode 100644 tests/Gigya.Microdot.UnitTests/Discovery/Rewrite/DiscoveryTests.cs create mode 100644 tests/Gigya.Microdot.UnitTests/Discovery/Rewrite/LoadBalancerTests.cs create mode 100644 tests/Gigya.Microdot.UnitTests/Discovery/Rewrite/NewConsulDiscoveryMasterFallBackTest.cs create mode 100644 tests/Gigya.Microdot.UnitTests/Discovery/Rewrite/NewServiceDiscoveryConfigChangeTest.cs diff --git a/Gigya.Microdot.Configuration/ConfigurationLocationsParser.cs b/Gigya.Microdot.Configuration/ConfigurationLocationsParser.cs index 9e64a09a..c84f43c6 100644 --- a/Gigya.Microdot.Configuration/ConfigurationLocationsParser.cs +++ b/Gigya.Microdot.Configuration/ConfigurationLocationsParser.cs @@ -62,15 +62,15 @@ private class ErrorAggregator public string ConfigRoot { get; } public string LoadPathsFilePath { get; } - public ConfigurationLocationsParser(IEnvironment environment, IFileSystem fileSystemInstance, IEnvironmentVariableProvider environmentVariableProvider) + public ConfigurationLocationsParser(IFileSystem fileSystemInstance, IEnvironmentVariableProvider environmentVariableProvider) { AppName = CurrentApplicationInfo.Name; - environment.SetEnvironmentVariableForProcess("AppName", CurrentApplicationInfo.Name); + environmentVariableProvider.SetEnvironmentVariableForProcess("AppName", CurrentApplicationInfo.Name); ConfigRoot = environmentVariableProvider.GetEnvironmentVariable(GIGYA_CONFIG_ROOT); if (string.IsNullOrEmpty(ConfigRoot)) - ConfigRoot = environment.PlatformSpecificPathPrefix + GIGYA_CONFIG_ROOT_DEFAULT; + ConfigRoot = environmentVariableProvider.PlatformSpecificPathPrefix + GIGYA_CONFIG_ROOT_DEFAULT; LoadPathsFilePath = environmentVariableProvider.GetEnvironmentVariable(GIGYA_CONFIG_PATHS_FILE); @@ -84,7 +84,7 @@ public ConfigurationLocationsParser(IEnvironment environment, IFileSystem fileSy var configPathDeclarations = ParseAndValidateConfigLines(LoadPathsFilePath, fileSystemInstance); - ConfigFileDeclarations = ExpandConfigPathDeclarations(environmentVariableProvider, configPathDeclarations, environment.PlatformSpecificPathPrefix).ToArray(); + ConfigFileDeclarations = ExpandConfigPathDeclarations(environmentVariableProvider, configPathDeclarations, environmentVariableProvider.PlatformSpecificPathPrefix).ToArray(); } diff --git a/Gigya.Microdot.Configuration/EnvironmentVariableProvider.cs b/Gigya.Microdot.Configuration/EnvironmentVariableProvider.cs index c482cd88..895b349c 100644 --- a/Gigya.Microdot.Configuration/EnvironmentVariableProvider.cs +++ b/Gigya.Microdot.Configuration/EnvironmentVariableProvider.cs @@ -20,36 +20,89 @@ // POSSIBILITY OF SUCH DAMAGE. #endregion +using System; +using System.Linq; using Gigya.Common.Contracts.Exceptions; using Gigya.Microdot.Interfaces.Configuration; using Gigya.Microdot.Interfaces.SystemWrappers; +using Gigya.Microdot.SharedLogic.Exceptions; +using Newtonsoft.Json.Linq; namespace Gigya.Microdot.Configuration { public class EnvironmentVariableProvider : IEnvironmentVariableProvider - { - private readonly IEnvironment _environment; - - public EnvironmentVariableProvider(IEnvironment environment, EnvironmentVariablesFileReader fileReader) + { + private const string GIGYA_ENV_VARS_FILE = "GIGYA_ENVVARS_FILE"; + private const string ENV_FILEPATH = "{0}/gigya/environmentVariables.json"; + + private IFileSystem FileSystem { get; } + + public EnvironmentVariableProvider(IFileSystem fileSystem) { - _environment = environment; + FileSystem = fileSystem; + PlatformSpecificPathPrefix = Environment.OSVersion.Platform == PlatformID.Unix ? "/etc" : "D:"; + + var locEnvFilePath = GetEnvironmentVariable(GIGYA_ENV_VARS_FILE); - fileReader.ReadFromFile(); - DataCenter = environment.GetEnvironmentVariable("DC"); - DeploymentEnvironment = environment.GetEnvironmentVariable("ENV"); - ConsulAddress = environment.GetEnvironmentVariable("CONSUL"); + if (string.IsNullOrEmpty(locEnvFilePath)) + { + locEnvFilePath = string.Format(ENV_FILEPATH, PlatformSpecificPathPrefix); + } - if (string.IsNullOrEmpty(DataCenter) || string.IsNullOrEmpty(DeploymentEnvironment)) - throw new EnvironmentException("One or more of the following environment variables, which are required, have not been set: %DC%, %ENV%"); + ReadFromFile(locEnvFilePath); + + DataCenter = GetEnvironmentVariable("ZONE") ?? GetEnvironmentVariable("DC"); + DeploymentEnvironment = GetEnvironmentVariable("ENV"); } - public string ConsulAddress { get; } + public void SetEnvironmentVariableForProcess(string name, string value) + { + Environment.SetEnvironmentVariable(name, value.ToLower(), EnvironmentVariableTarget.Process); + } + + public string GetEnvironmentVariable(string name) { return Environment.GetEnvironmentVariable(name)?.ToLower(); } + public string PlatformSpecificPathPrefix { get; } + + [Obsolete("To be deleted on version 2.0")] public string DataCenter { get; } + [Obsolete("To be deleted on version 2.0")] public string DeploymentEnvironment { get; } - public string GetEnvironmentVariable(string name) { return _environment.GetEnvironmentVariable(name); } + + /// + /// Reads each property in file and sets its environment variable. + /// + /// Names of environment variables read from file + public void ReadFromFile(string locEnvFilePath) + { + JObject envVarsObject; + + try + { + var text = FileSystem.TryReadAllTextFromFile(locEnvFilePath); + + if (string.IsNullOrEmpty(text)) + return; + + envVarsObject = JObject.Parse(text); + } + catch (Exception ex) + { + throw new ConfigurationException($"Missing or invalid configuration file: {locEnvFilePath}", ex); + } + + if (envVarsObject == null) + return; + + var properties = envVarsObject.Properties().Where(a => a.HasValues).ToArray(); + + foreach (var property in properties) + { + SetEnvironmentVariableForProcess(property.Name, property.Value.Value()); + } + } } } \ No newline at end of file diff --git a/Gigya.Microdot.Configuration/EnvironmentVariablesFileReader.cs b/Gigya.Microdot.Configuration/EnvironmentVariablesFileReader.cs index 630270cc..cbf69fae 100644 --- a/Gigya.Microdot.Configuration/EnvironmentVariablesFileReader.cs +++ b/Gigya.Microdot.Configuration/EnvironmentVariablesFileReader.cs @@ -22,6 +22,7 @@ using System; using System.Linq; +using Gigya.Microdot.Interfaces.Configuration; using Gigya.Microdot.Interfaces.SystemWrappers; using Gigya.Microdot.SharedLogic.Exceptions; using Newtonsoft.Json.Linq; @@ -42,23 +43,22 @@ public class EnvironmentVariablesFileReader private const string ENV_FILEPATH = "{0}/gigya/environmentVariables.json"; private readonly string locEnvFilePath; - private IEnvironment Environment { get; } + private IEnvironmentVariableProvider EnvironmentVariableProvider { get; } private IFileSystem FileSystem { get; } - /// /// Parses the content of environment variables file content. /// - public EnvironmentVariablesFileReader(IFileSystem fileSystem, IEnvironment environment) + public EnvironmentVariablesFileReader(IFileSystem fileSystem, IEnvironmentVariableProvider environmentVariableProvider) { - locEnvFilePath = environment.GetEnvironmentVariable(GIGYA_ENV_VARS_FILE); + locEnvFilePath = environmentVariableProvider.GetEnvironmentVariable(GIGYA_ENV_VARS_FILE); if (string.IsNullOrEmpty(locEnvFilePath)) { - locEnvFilePath = string.Format(ENV_FILEPATH, environment.PlatformSpecificPathPrefix); + locEnvFilePath = string.Format(ENV_FILEPATH, environmentVariableProvider.PlatformSpecificPathPrefix); } - - Environment = environment; + + EnvironmentVariableProvider = environmentVariableProvider; FileSystem = fileSystem; } @@ -92,7 +92,7 @@ public void ReadFromFile() foreach (var property in properties) { - Environment.SetEnvironmentVariableForProcess(property.Name, property.Value.Value()); + EnvironmentVariableProvider.SetEnvironmentVariableForProcess(property.Name, property.Value.Value()); } } } diff --git a/Gigya.Microdot.Fakes/DateTimeFake.cs b/Gigya.Microdot.Fakes/DateTimeFake.cs index 62c7ebed..74417f4c 100644 --- a/Gigya.Microdot.Fakes/DateTimeFake.cs +++ b/Gigya.Microdot.Fakes/DateTimeFake.cs @@ -22,14 +22,15 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Gigya.Microdot.Interfaces.SystemWrappers; namespace Gigya.Microdot.Fakes { - public class DateTimeFake: IDateTime + public class DateTimeFake : IDateTime { - public DateTime UtcNow { get; set; } + public DateTime UtcNow { get; set; } = DateTime.UtcNow; private TaskCompletionSource _delayTask = new TaskCompletionSource(); @@ -46,10 +47,28 @@ public DateTimeFake(bool manualDelay) _manualDelay = manualDelay; } - public Task Delay(TimeSpan delay) + public Task Delay(TimeSpan delay) => Delay(delay, default(CancellationToken)); + public async Task Delay(TimeSpan delay, CancellationToken cancellationToken = default(CancellationToken)) { DelaysRequested.Add(delay); - return _manualDelay ? _delayTask.Task : Task.Delay(delay); + + if (_manualDelay) + await _delayTask.Task; + else + await Task.Delay(delay, cancellationToken); + + UtcNow += delay; + } + + public async Task DelayUntil(DateTime until, CancellationToken cancellationToken = default(CancellationToken)) + { + TimeSpan delayTime = until - UtcNow; + + if (delayTime > TimeSpan.Zero) + { + await Delay(delayTime, cancellationToken).ConfigureAwait(false); + UtcNow += delayTime; + } } /// diff --git a/Gigya.Microdot.Fakes/Discovery/AlwaysLocalHost.cs b/Gigya.Microdot.Fakes/Discovery/AlwaysLocalHost.cs index 329a69f0..8e1d1f1f 100644 --- a/Gigya.Microdot.Fakes/Discovery/AlwaysLocalHost.cs +++ b/Gigya.Microdot.Fakes/Discovery/AlwaysLocalHost.cs @@ -27,9 +27,9 @@ namespace Gigya.Microdot.Fakes.Discovery { public class AlwaysLocalHost : IDiscoverySourceLoader { - public IServiceDiscoverySource GetDiscoverySource(ServiceDeployment serviceDeployment, ServiceDiscoveryConfig serviceDiscoveryConfig) + public IServiceDiscoverySource GetDiscoverySource(DeploymentIdentifier deploymentIdentifier, ServiceDiscoveryConfig serviceDiscoveryConfig) { - return new LocalDiscoverySource(serviceDeployment); + return new LocalDiscoverySource(deploymentIdentifier); } } } \ No newline at end of file diff --git a/Gigya.Microdot.ServiceDiscovery/ServiceDeployment.cs b/Gigya.Microdot.Fakes/Discovery/AlwaysLocalHostDiscovery.cs similarity index 51% rename from Gigya.Microdot.ServiceDiscovery/ServiceDeployment.cs rename to Gigya.Microdot.Fakes/Discovery/AlwaysLocalHostDiscovery.cs index 4f6da804..9f0003fc 100644 --- a/Gigya.Microdot.ServiceDiscovery/ServiceDeployment.cs +++ b/Gigya.Microdot.Fakes/Discovery/AlwaysLocalHostDiscovery.cs @@ -19,50 +19,37 @@ // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE // POSSIBILITY OF SUCH DAMAGE. #endregion -namespace Gigya.Microdot.ServiceDiscovery + +using System; +using System.Threading.Tasks; +using Gigya.Microdot.ServiceDiscovery; +using Gigya.Microdot.ServiceDiscovery.Config; +using Gigya.Microdot.ServiceDiscovery.Rewrite; +using Gigya.Microdot.SharedLogic.Rewrite; + +namespace Gigya.Microdot.Fakes.Discovery { - public class ServiceDeployment + public class AlwaysLocalhostDiscovery : IDiscovery { - public string DeploymentEnvironment { get; } - public string ServiceName { get; } - + private Func _createLoadBalancer {get;} - public ServiceDeployment(string serviceName, string deploymentEnvironment) + public AlwaysLocalhostDiscovery(Func createLoadBalancer) { - DeploymentEnvironment = deploymentEnvironment.ToLower(); - ServiceName = serviceName; + _createLoadBalancer = createLoadBalancer; } - - public override string ToString() + public ILoadBalancer CreateLoadBalancer(DeploymentIdentifier deploymentIdentifier, ReachabilityCheck reachabilityCheck, TrafficRoutingStrategy trafficRoutingStrategy) { - return $"{ServiceName}-{DeploymentEnvironment}"; + return _createLoadBalancer(deploymentIdentifier, new LocalNodeSource(), reachabilityCheck, trafficRoutingStrategy); } - - public override bool Equals(object obj) + public async Task GetNodes(DeploymentIdentifier deploymentIdentifier) { - if (ReferenceEquals(null, obj)) - return false; - - if (ReferenceEquals(this, obj)) - return true; - - ServiceDeployment other = obj as ServiceDeployment; - - if (other == null) - return false; - - return DeploymentEnvironment == other.DeploymentEnvironment && ServiceName == other.ServiceName; + return new LocalNodeSource().GetNodes(); } - - public override int GetHashCode() + public void Dispose() { - unchecked - { - return ((DeploymentEnvironment?.GetHashCode() ?? 0) * 397) ^ (ServiceName?.GetHashCode() ?? 0); - } } } } \ No newline at end of file diff --git a/Gigya.Microdot.Fakes/Discovery/LocalhostServiceDiscovery.cs b/Gigya.Microdot.Fakes/Discovery/LocalhostServiceDiscovery.cs index 082ce51f..4147874d 100644 --- a/Gigya.Microdot.Fakes/Discovery/LocalhostServiceDiscovery.cs +++ b/Gigya.Microdot.Fakes/Discovery/LocalhostServiceDiscovery.cs @@ -20,11 +20,15 @@ // POSSIBILITY OF SUCH DAMAGE. #endregion +using System; +using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Threading.Tasks.Dataflow; -using Gigya.Microdot.Interfaces.HttpService; using Gigya.Microdot.ServiceDiscovery; +using Gigya.Microdot.ServiceDiscovery.Rewrite; +using Gigya.Microdot.SharedLogic.HttpService; +using Gigya.Microdot.SharedLogic.Rewrite; namespace Gigya.Microdot.Fakes.Discovery { @@ -47,4 +51,5 @@ public class LocalhostServiceDiscovery : IServiceDiscovery public Task GetAllEndPoints() => allHosts; } + } diff --git a/Gigya.Microdot.Fakes/Gigya.Microdot.Fakes.csproj b/Gigya.Microdot.Fakes/Gigya.Microdot.Fakes.csproj index 7fa04c59..ebd36b1f 100644 --- a/Gigya.Microdot.Fakes/Gigya.Microdot.Fakes.csproj +++ b/Gigya.Microdot.Fakes/Gigya.Microdot.Fakes.csproj @@ -44,6 +44,7 @@ Properties\SolutionVersion.cs + diff --git a/Gigya.Microdot.Hosting/Events/ServiceCallEvent.cs b/Gigya.Microdot.Hosting/Events/ServiceCallEvent.cs index 25087d62..6b33d2e8 100644 --- a/Gigya.Microdot.Hosting/Events/ServiceCallEvent.cs +++ b/Gigya.Microdot.Hosting/Events/ServiceCallEvent.cs @@ -25,8 +25,8 @@ using System.Linq; using System.Text.RegularExpressions; using Gigya.Microdot.Interfaces.Events; -using Gigya.Microdot.Interfaces.HttpService; using Gigya.Microdot.SharedLogic.Events; +using Gigya.Microdot.SharedLogic.HttpService; namespace Gigya.Microdot.Hosting.Events { diff --git a/Gigya.Microdot.Hosting/HttpService/Endpoints/ConfigurationResponseBuilder.cs b/Gigya.Microdot.Hosting/HttpService/Endpoints/ConfigurationResponseBuilder.cs index 068b8384..8bdb57ec 100644 --- a/Gigya.Microdot.Hosting/HttpService/Endpoints/ConfigurationResponseBuilder.cs +++ b/Gigya.Microdot.Hosting/HttpService/Endpoints/ConfigurationResponseBuilder.cs @@ -162,7 +162,7 @@ private Dictionary GetEnvironmentVariables() return Environment.GetEnvironmentVariables() .OfType() .Select(x => new { Name = (string)x.Key, Value = (string)x.Value }) - .Where(x => x.Name.ToUpper() == "DC" || x.Name.ToUpper() == "ENV" || x.Name.ToUpper().Contains("GIGYA")) + .Where(x => x.Name.ToUpper() == "DC" || x.Name.ToUpper() == "ZONE" || x.Name.ToUpper() == "REGION" || x.Name.ToUpper() == "ENV" || x.Name.ToUpper().Contains("GIGYA")) .OrderBy(x => x.Name) .ToDictionary(x => x.Name, x => x.Value); } diff --git a/Gigya.Microdot.Hosting/HttpService/Endpoints/SchemaEndpoint.cs b/Gigya.Microdot.Hosting/HttpService/Endpoints/SchemaEndpoint.cs index a20a4f1c..26959527 100644 --- a/Gigya.Microdot.Hosting/HttpService/Endpoints/SchemaEndpoint.cs +++ b/Gigya.Microdot.Hosting/HttpService/Endpoints/SchemaEndpoint.cs @@ -20,7 +20,6 @@ // POSSIBILITY OF SUCH DAMAGE. #endregion -using System.Linq; using System.Net; using System.Threading.Tasks; using Gigya.Common.Contracts.HttpService; @@ -28,16 +27,14 @@ namespace Gigya.Microdot.Hosting.HttpService.Endpoints { - public class SchemaEndpoint : ICustomEndpoint { private readonly string _jsonSchema; - public SchemaEndpoint(IServiceInterfaceMapper mapper) + public SchemaEndpoint(ServiceSchema schemaProvider) { - _jsonSchema = JsonConvert.SerializeObject(new ServiceSchema(mapper.ServiceInterfaceTypes.ToArray()), new JsonSerializerSettings{Formatting = Formatting.Indented, NullValueHandling = NullValueHandling.Ignore}); - } - + _jsonSchema = JsonConvert.SerializeObject(schemaProvider, new JsonSerializerSettings{Formatting = Formatting.Indented, NullValueHandling = NullValueHandling.Ignore}); + } public async Task TryHandle(HttpListenerContext context, WriteResponseDelegate writeResponse) { diff --git a/Gigya.Microdot.Hosting/HttpService/HttpServiceListener.cs b/Gigya.Microdot.Hosting/HttpService/HttpServiceListener.cs index 24da1c58..203c99f8 100644 --- a/Gigya.Microdot.Hosting/HttpService/HttpServiceListener.cs +++ b/Gigya.Microdot.Hosting/HttpService/HttpServiceListener.cs @@ -33,16 +33,18 @@ using System.Threading.Tasks; using Gigya.Common.Contracts; using Gigya.Common.Contracts.Exceptions; +using Gigya.Common.Contracts.HttpService; using Gigya.Microdot.Hosting.Events; using Gigya.Microdot.Hosting.HttpService.Endpoints; using Gigya.Microdot.Interfaces.Configuration; using Gigya.Microdot.Interfaces.Events; -using Gigya.Microdot.Interfaces.HttpService; using Gigya.Microdot.Interfaces.Logging; +using Gigya.Microdot.Interfaces.SystemWrappers; using Gigya.Microdot.SharedLogic; using Gigya.Microdot.SharedLogic.Configurations; using Gigya.Microdot.SharedLogic.Events; using Gigya.Microdot.SharedLogic.Exceptions; +using Gigya.Microdot.SharedLogic.HttpService; using Gigya.Microdot.SharedLogic.Measurement; using Gigya.Microdot.SharedLogic.Security; using Metrics; @@ -83,10 +85,11 @@ public sealed class HttpServiceListener : IDisposable private ILog Log { get; } private IEventPublisher EventPublisher { get; } private IEnumerable CustomEndpoints { get; } - private IEnvironmentVariableProvider EnvironmentVariableProvider { get; } + private IEnvironment Environment { get; } private JsonExceptionSerializer ExceptionSerializer { get; } private Func LoadSheddingConfig { get; } + private ServiceSchema ServiceSchema { get; } private readonly Timer _serializationTime; private readonly Timer _deserializationTime; @@ -99,10 +102,14 @@ public sealed class HttpServiceListener : IDisposable public HttpServiceListener(IActivator activator, IWorker worker, IServiceEndPointDefinition serviceEndPointDefinition, ICertificateLocator certificateLocator, ILog log, IEventPublisher eventPublisher, - IEnumerable customEndpoints, IEnvironmentVariableProvider environmentVariableProvider, - IServerRequestPublisher serverRequestPublisher, - JsonExceptionSerializer exceptionSerializer, Func loadSheddingConfig) + IEnumerable customEndpoints, IEnvironment environment, + JsonExceptionSerializer exceptionSerializer, + ServiceSchema serviceSchema, + Func loadSheddingConfig, + IServerRequestPublisher serverRequestPublisher) + { + ServiceSchema = serviceSchema; _serverRequestPublisher = serverRequestPublisher; ServiceEndPointDefinition = serviceEndPointDefinition; Worker = worker; @@ -110,7 +117,7 @@ public HttpServiceListener(IActivator activator, IWorker worker, IServiceEndPoin Log = log; EventPublisher = eventPublisher; CustomEndpoints = customEndpoints.ToArray(); - EnvironmentVariableProvider = environmentVariableProvider; + Environment = environment; ExceptionSerializer = exceptionSerializer; LoadSheddingConfig = loadSheddingConfig; @@ -356,12 +363,12 @@ private static IEnumerable GetAllExceptions(Exception ex) private void ValidateRequest(HttpListenerContext context) { - var clientVersion = context.Request.Headers[GigyaHttpHeaders.Version]; + var clientVersion = context.Request.Headers[GigyaHttpHeaders.ProtocolVersion]; - if (clientVersion != null && clientVersion != HttpServiceRequest.Version) + if (clientVersion != null && clientVersion != HttpServiceRequest.ProtocolVersion) { _failureCounter.Increment("ProtocolVersionMismatch"); - throw new RequestException($"Client protocol version {clientVersion} is not supported by the server protocol version {HttpServiceRequest.Version}."); + throw new RequestException($"Client protocol version {clientVersion} is not supported by the server protocol version {HttpServiceRequest.ProtocolVersion}."); } if (context.Request.HttpMethod != "POST") @@ -416,17 +423,19 @@ private async Task CheckSecureConnection(HttpListenerContext context) private async Task TryWriteResponse(HttpListenerContext context, string data, HttpStatusCode httpStatus = HttpStatusCode.OK, string contentType = "application/json") { - context.Response.Headers.Add(GigyaHttpHeaders.Version, HttpServiceRequest.Version); + context.Response.Headers.Add(GigyaHttpHeaders.ProtocolVersion, HttpServiceRequest.ProtocolVersion); var body = Encoding.UTF8.GetBytes(data ?? ""); context.Response.StatusCode = (int)httpStatus; context.Response.ContentLength64 = body.Length; context.Response.ContentType = contentType; - context.Response.Headers.Add(GigyaHttpHeaders.DataCenter, EnvironmentVariableProvider.DataCenter); - context.Response.Headers.Add(GigyaHttpHeaders.Environment, EnvironmentVariableProvider.DeploymentEnvironment); + context.Response.Headers.Add(GigyaHttpHeaders.DataCenter, Environment.Zone); + context.Response.Headers.Add(GigyaHttpHeaders.Environment, Environment.DeploymentEnvironment); context.Response.Headers.Add(GigyaHttpHeaders.ServiceVersion, CurrentApplicationInfo.Version.ToString()); context.Response.Headers.Add(GigyaHttpHeaders.ServerHostname, CurrentApplicationInfo.HostName); + context.Response.Headers.Add(GigyaHttpHeaders.SchemaHash, ServiceSchema.Hash); + try { await context.Response.OutputStream.WriteAsync(body, 0, body.Length); diff --git a/Gigya.Microdot.Hosting/HttpService/IServiceEndPointDefinition.cs b/Gigya.Microdot.Hosting/HttpService/IServiceEndPointDefinition.cs index 9f036857..7748da12 100644 --- a/Gigya.Microdot.Hosting/HttpService/IServiceEndPointDefinition.cs +++ b/Gigya.Microdot.Hosting/HttpService/IServiceEndPointDefinition.cs @@ -23,7 +23,7 @@ using System; using System.Collections.Generic; using Gigya.Common.Contracts.HttpService; -using Gigya.Microdot.Interfaces.HttpService; +using Gigya.Microdot.SharedLogic.HttpService; namespace Gigya.Microdot.Hosting.HttpService { diff --git a/Gigya.Microdot.Hosting/HttpService/ServerRequestPublisher.cs b/Gigya.Microdot.Hosting/HttpService/ServerRequestPublisher.cs index 3f25809b..9dd1991f 100644 --- a/Gigya.Microdot.Hosting/HttpService/ServerRequestPublisher.cs +++ b/Gigya.Microdot.Hosting/HttpService/ServerRequestPublisher.cs @@ -5,9 +5,8 @@ using System.Linq; using Gigya.Microdot.Hosting.Events; using Gigya.Microdot.Interfaces.Events; -using Gigya.Microdot.Interfaces.HttpService; using Gigya.Microdot.SharedLogic.Events; -using Newtonsoft.Json; +using Gigya.Microdot.SharedLogic.HttpService; namespace Gigya.Microdot.Hosting.HttpService { diff --git a/Gigya.Microdot.Hosting/HttpService/ServiceEndPointDefinition.cs b/Gigya.Microdot.Hosting/HttpService/ServiceEndPointDefinition.cs index 9cf5114d..44b8ed3e 100644 --- a/Gigya.Microdot.Hosting/HttpService/ServiceEndPointDefinition.cs +++ b/Gigya.Microdot.Hosting/HttpService/ServiceEndPointDefinition.cs @@ -28,10 +28,10 @@ using Gigya.Common.Contracts.Exceptions; using Gigya.Common.Contracts.HttpService; using Gigya.Microdot.Interfaces; -using Gigya.Microdot.Interfaces.HttpService; using Gigya.Microdot.ServiceDiscovery.Config; using Gigya.Microdot.SharedLogic; using Gigya.Microdot.SharedLogic.Exceptions; +using Gigya.Microdot.SharedLogic.HttpService; namespace Gigya.Microdot.Hosting.HttpService { @@ -70,7 +70,7 @@ public ServiceEndPointDefinition(IServiceInterfaceMapper mapper, ServiceNames = serviceInterfaces .Where(i => i.GetCustomAttribute() != null) - .ToDictionary(x => x, x => x.GetCustomAttribute().Name ?? x.Name); + .ToDictionary(x => x, x => x.Name); var interfacePorts = serviceInterfaces.Select(i => { diff --git a/Gigya.Microdot.Hosting/HttpService/ServiceMethodResolver.cs b/Gigya.Microdot.Hosting/HttpService/ServiceMethodResolver.cs index 07e4b758..5ee6b13a 100644 --- a/Gigya.Microdot.Hosting/HttpService/ServiceMethodResolver.cs +++ b/Gigya.Microdot.Hosting/HttpService/ServiceMethodResolver.cs @@ -26,7 +26,7 @@ using System.Linq; using System.Reflection; using Gigya.Common.Contracts.Exceptions; -using Gigya.Microdot.Interfaces.HttpService; +using Gigya.Microdot.SharedLogic.HttpService; namespace Gigya.Microdot.Hosting.HttpService { diff --git a/Gigya.Microdot.Interfaces/Configuration/IEnvironmentVariableProvider.cs b/Gigya.Microdot.Interfaces/Configuration/IEnvironmentVariableProvider.cs index 8c1c0fbc..8f6bbefe 100644 --- a/Gigya.Microdot.Interfaces/Configuration/IEnvironmentVariableProvider.cs +++ b/Gigya.Microdot.Interfaces/Configuration/IEnvironmentVariableProvider.cs @@ -19,6 +19,9 @@ // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE // POSSIBILITY OF SUCH DAMAGE. #endregion + +using System; + namespace Gigya.Microdot.Interfaces.Configuration { public interface IEnvironmentVariableProvider @@ -26,18 +29,19 @@ public interface IEnvironmentVariableProvider /// /// Initialized with environment variable DC /// + [Obsolete("To be removed on Microdot version 2.0. Use IEnvironment.Zone instead")] string DataCenter { get; } /// /// Initialized with environment variable ENV /// + [Obsolete("To be removed on Microdot version 2.0. Use IEnvironment.DeploymentEnvironment instead")] string DeploymentEnvironment { get; } - /// - /// Initialized with environment variable CONSUL - /// - string ConsulAddress { get; } - string GetEnvironmentVariable(string name); + + void SetEnvironmentVariableForProcess(string name, string value); + + string PlatformSpecificPathPrefix { get; } } } \ No newline at end of file diff --git a/Gigya.Microdot.Interfaces/Events/IEvent.cs b/Gigya.Microdot.Interfaces/Events/IEvent.cs index bef364d7..ef549ab9 100644 --- a/Gigya.Microdot.Interfaces/Events/IEvent.cs +++ b/Gigya.Microdot.Interfaces/Events/IEvent.cs @@ -23,6 +23,7 @@ using System; using Gigya.Microdot.Interfaces.Configuration; using Gigya.Microdot.Interfaces.Logging; +using Gigya.Microdot.Interfaces.SystemWrappers; namespace Gigya.Microdot.Interfaces.Events { @@ -36,7 +37,7 @@ public interface IEvent EventConfiguration Configuration { get; set; } - IEnvironmentVariableProvider EnvironmentVariableProvider { get; set; } + IEnvironment Environment { get; set; } IStackTraceEnhancer StackTraceEnhancer { get; set; } } diff --git a/Gigya.Microdot.Interfaces/Gigya.Microdot.Interfaces.csproj b/Gigya.Microdot.Interfaces/Gigya.Microdot.Interfaces.csproj index 343b3224..0bcef000 100644 --- a/Gigya.Microdot.Interfaces/Gigya.Microdot.Interfaces.csproj +++ b/Gigya.Microdot.Interfaces/Gigya.Microdot.Interfaces.csproj @@ -54,10 +54,6 @@ - - - - diff --git a/Gigya.Microdot.Interfaces/SystemWrappers/IDateTime.cs b/Gigya.Microdot.Interfaces/SystemWrappers/IDateTime.cs index 3b436ed7..70c3e72a 100644 --- a/Gigya.Microdot.Interfaces/SystemWrappers/IDateTime.cs +++ b/Gigya.Microdot.Interfaces/SystemWrappers/IDateTime.cs @@ -21,6 +21,7 @@ #endregion using System; +using System.Threading; using System.Threading.Tasks; namespace Gigya.Microdot.Interfaces.SystemWrappers @@ -28,6 +29,11 @@ namespace Gigya.Microdot.Interfaces.SystemWrappers public interface IDateTime { DateTime UtcNow { get; } + + [Obsolete("This method will be removed on Microdot version 2.0. Please use Delay(delay, cancellationToken = default(CancellationToke)) instead.")] Task Delay(TimeSpan delay); + Task Delay(TimeSpan delay, CancellationToken cancellationToken = default(CancellationToken)); + + Task DelayUntil(DateTime until, CancellationToken cancellationToken = default(CancellationToken)); } } diff --git a/Gigya.Microdot.Interfaces/SystemWrappers/IEnvironment.cs b/Gigya.Microdot.Interfaces/SystemWrappers/IEnvironment.cs index 32b11602..69167f04 100644 --- a/Gigya.Microdot.Interfaces/SystemWrappers/IEnvironment.cs +++ b/Gigya.Microdot.Interfaces/SystemWrappers/IEnvironment.cs @@ -19,14 +19,35 @@ // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE // POSSIBILITY OF SUCH DAMAGE. #endregion + +using System; + namespace Gigya.Microdot.Interfaces.SystemWrappers { public interface IEnvironment { + /// + /// The current Region this application runs in, e.g. "us1", "eu2". + /// Initialized from the environment variable "REGION". + /// + string Region { get; } + + /// + /// The current Zone this application runs in, e.g. "us1a" or "eu2c". Initialized from the environment variable "ZONE". + /// + string Zone { get; } + + /// + /// The current environment this application runs in, e.g. "prod", "st1" or "canary". Initialized from the environment variable "ENV". + /// + string DeploymentEnvironment { get; } + + string ConsulAddress { get; } + + [Obsolete("To be removed on Microdot version 2.0. Use IEnvironmentVariableProvider.SetEnvironmentVariableForProcess instead")] void SetEnvironmentVariableForProcess(string name, string value); + [Obsolete("To be removed on Microdot version 2.0. Use IEnvironmentVariableProvider.SetEnvironmentVariableForProcess instead")] string GetEnvironmentVariable(string name); - - string PlatformSpecificPathPrefix { get; } } } \ No newline at end of file diff --git a/Gigya.Microdot.Ninject/MicrodotModule.cs b/Gigya.Microdot.Ninject/MicrodotModule.cs index 2b2567c1..c7299a63 100644 --- a/Gigya.Microdot.Ninject/MicrodotModule.cs +++ b/Gigya.Microdot.Ninject/MicrodotModule.cs @@ -22,17 +22,24 @@ using System; using System.Collections.Concurrent; +using System.Linq; +using Gigya.Common.Contracts.HttpService; using Gigya.Microdot.Configuration; +using Gigya.Microdot.Hosting.HttpService; using Gigya.Microdot.ServiceDiscovery; using Gigya.Microdot.ServiceDiscovery.HostManagement; +using Gigya.Microdot.ServiceDiscovery.Rewrite; using Gigya.Microdot.ServiceProxy; using Gigya.Microdot.SharedLogic; using Gigya.Microdot.SharedLogic.Monitor; +using Gigya.Microdot.SharedLogic.Rewrite; using Metrics; using Ninject; using Ninject.Activation; using Ninject.Extensions.Factory; using Ninject.Modules; +using ConsulClient = Gigya.Microdot.ServiceDiscovery.ConsulClient; +using IConsulClient = Gigya.Microdot.ServiceDiscovery.IConsulClient; namespace Gigya.Microdot.Ninject { @@ -47,6 +54,7 @@ public class MicrodotModule : NinjectModule { typeof(ConsulDiscoverySource), typeof(RemoteHostPool), + typeof(LoadBalancer), typeof(ConfigDiscoverySource) }; @@ -64,10 +72,11 @@ public override void Load() Kernel.Load(); this.BindClassesAsSingleton(NonSingletonBaseTypes, typeof(ConfigurationAssembly), typeof(ServiceProxyAssembly)); - this.BindInterfacesAsSingleton(NonSingletonBaseTypes, typeof(ConfigurationAssembly), typeof(ServiceProxyAssembly), typeof(SharedLogicAssembly),typeof(ServiceDiscoveryAssembly)); - + this.BindInterfacesAsSingleton(NonSingletonBaseTypes, typeof(ConfigurationAssembly), typeof(ServiceProxyAssembly), typeof(SharedLogicAssembly), typeof(ServiceDiscoveryAssembly)); + Bind().ToFactory(); + Kernel.BindPerKey(); Kernel.BindPerKey(); Kernel.BindPerString(); Kernel.BindPerString(); @@ -80,9 +89,21 @@ public override void Load() Bind().To().InTransientScope(); Bind().To().InTransientScope(); + Bind().To().InTransientScope(); + Bind().To().InTransientScope(); + Bind().To().InSingletonScope(); + + Rebind() + .To().InSingletonScope(); + + Kernel.Rebind().To().InTransientScope(); Kernel.Load(); Kernel.Load(); + + // ServiceSchema is at ServiceContracts, and cannot be depended on IServiceInterfaceMapper, which belongs to Microdot + Kernel.Rebind() + .ToMethod(c =>new ServiceSchema(c.Kernel.Get().ServiceInterfaceTypes.ToArray())).InSingletonScope(); } diff --git a/Gigya.Microdot.Orleans.Hosting/ClusterIdentity.cs b/Gigya.Microdot.Orleans.Hosting/ClusterIdentity.cs index 3276d925..20463660 100644 --- a/Gigya.Microdot.Orleans.Hosting/ClusterIdentity.cs +++ b/Gigya.Microdot.Orleans.Hosting/ClusterIdentity.cs @@ -24,6 +24,7 @@ using Gigya.Common.Contracts.Exceptions; using Gigya.Microdot.Interfaces.Configuration; using Gigya.Microdot.Interfaces.Logging; +using Gigya.Microdot.Interfaces.SystemWrappers; using Gigya.Microdot.SharedLogic; namespace Gigya.Microdot.Orleans.Hosting @@ -47,13 +48,13 @@ public class ClusterIdentity /// /// Performs discovery of services in the silo and populates the class' static members with information about them. /// - public ClusterIdentity(ServiceArguments serviceArguments, ILog log, IEnvironmentVariableProvider environmentVariableProvider) + public ClusterIdentity(ServiceArguments serviceArguments, ILog log, IEnvironment environment) { if (serviceArguments.SiloClusterMode != SiloClusterMode.ZooKeeper) return; - string dc = environmentVariableProvider.DataCenter; - string env = environmentVariableProvider.DeploymentEnvironment; + string dc = environment.Zone; + string env = environment.DeploymentEnvironment; diff --git a/Gigya.Microdot.ServiceDiscovery/Config/ConsulConfig.cs b/Gigya.Microdot.ServiceDiscovery/Config/ConsulConfig.cs index 6f1f2654..ad10d801 100644 --- a/Gigya.Microdot.ServiceDiscovery/Config/ConsulConfig.cs +++ b/Gigya.Microdot.ServiceDiscovery/Config/ConsulConfig.cs @@ -3,6 +3,7 @@ namespace Gigya.Microdot.ServiceDiscovery.Config { + [Serializable] [ConfigurationRoot("Consul", RootStrategy.ReplaceClassNameWithPath)] public class ConsulConfig : IConfigObject @@ -10,23 +11,32 @@ public class ConsulConfig : IConfigObject /// /// Whether to Call Consul with long-polling, waiting for changes to occur, or to call it periodically /// + [Obsolete("To be deleted after discovery refactoring")] public bool LongPolling { get; set; } = false; /// /// Interval for reloading endpoints from Consul, - /// Used only when LongPolling=false + /// Used for Consul queries loop /// public TimeSpan ReloadInterval { get; set; } = TimeSpan.FromSeconds(1); + /// + /// Timeout passed to Consul telling it when to break long-polling. + /// + public TimeSpan HttpTimeout { get; set; } = TimeSpan.FromMinutes(2); + /// /// Time to wait for http response from Consul. /// When LongPolling=true, defines the maximum time to wait on long-polling. /// When LongPolling=false, defines the timeout for Consul http requests. + /// We take a few seconds more than to reduce the + /// risk of getting task cancelled exceptions before Consul gracefully timed out, + /// due to network latency or the process being overloaded. /// - public TimeSpan HttpTimeout { get; set; } = TimeSpan.FromMinutes(2); + public TimeSpan HttpTaskTimeout => HttpTimeout.Add(TimeSpan.FromSeconds(5)); /// - /// Interval for retrying access to surce (e.g. Consul) after an error has occured + /// Interval for retrying access to Consul after an error has occured /// public TimeSpan ErrorRetryInterval { get; set; } = TimeSpan.FromSeconds(1); } diff --git a/Gigya.Microdot.ServiceDiscovery/Config/DiscoveryConfig.cs b/Gigya.Microdot.ServiceDiscovery/Config/DiscoveryConfig.cs index 7fae8f86..b00bf384 100644 --- a/Gigya.Microdot.ServiceDiscovery/Config/DiscoveryConfig.cs +++ b/Gigya.Microdot.ServiceDiscovery/Config/DiscoveryConfig.cs @@ -50,20 +50,28 @@ public class DiscoveryConfig : IConfigObject /// public TimeSpan? RequestTimeout { get; set; } + /// + /// Time period to keep monitoring a deployed service after it was no longer requested + /// + public TimeSpan MonitoringLifetime { get; set; } = TimeSpan.FromMinutes(5); + /// /// When we lose connection to some endpoint, we wait this delay till we start trying to reconnect. /// + [Obsolete("To be deleted after discovery refactoring")] public double FirstAttemptDelaySeconds { get; set; } = 0.001; /// /// When retrying to reconnect to an endpoint, we use exponential backoff (e.g. 1,2,4,8ms, etc). Once that /// backoff reaches this value, it won't increase any more. /// + [Obsolete("To be deleted after discovery refactoring")] public double MaxAttemptDelaySeconds { get; set; } = 10; /// /// The factor of the exponential backoff when retrying connections to endpoints. /// + [Obsolete("To be deleted after discovery refactoring")] public double DelayMultiplier { get; set; } = 2; /// diff --git a/Gigya.Microdot.ServiceDiscovery/Config/ServiceDiscoveryConfig.cs b/Gigya.Microdot.ServiceDiscovery/Config/ServiceDiscoveryConfig.cs index 2f6e8f9e..be63ef07 100644 --- a/Gigya.Microdot.ServiceDiscovery/Config/ServiceDiscoveryConfig.cs +++ b/Gigya.Microdot.ServiceDiscovery/Config/ServiceDiscoveryConfig.cs @@ -46,17 +46,20 @@ public class ServiceDiscoveryConfig /// /// When we lose connection to some endpoint, we wait this delay till we start trying to reconnect. /// + [Obsolete("To be deleted after discovery refactoring")] public double? FirstAttemptDelaySeconds { get; set; } /// /// When retrying to reconnect to an endpoint, we use exponential backoff (e.g. 1,2,4,8ms, etc). Once that /// backoff reaches this value, it won't increase any more. /// + [Obsolete("To be deleted after discovery refactoring")] public double? MaxAttemptDelaySeconds { get; set; } /// /// The factor of the exponential backoff when retrying connections to endpoints. /// + [Obsolete("To be deleted after discovery refactoring")] public double? DelayMultiplier { get; set; } /// diff --git a/Gigya.Microdot.ServiceDiscovery/Config/ServiceScope.cs b/Gigya.Microdot.ServiceDiscovery/Config/ServiceScope.cs index 90fc36c7..27d555a8 100644 --- a/Gigya.Microdot.ServiceDiscovery/Config/ServiceScope.cs +++ b/Gigya.Microdot.ServiceDiscovery/Config/ServiceScope.cs @@ -19,6 +19,9 @@ // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE // POSSIBILITY OF SUCH DAMAGE. #endregion + +using System; + namespace Gigya.Microdot.ServiceDiscovery.Config { /// @@ -26,10 +29,14 @@ namespace Gigya.Microdot.ServiceDiscovery.Config /// public enum ServiceScope { - /// This Service is in entire DataCenter scope, and should get requests from any environment within this data-center. - DataCenter, + /// This Service is in entire Zone scope, and should get requests from any environment within this data-center. + [Obsolete("Use Zone instead")] + DataCenter = 0, + + /// This Service is in entire Zone scope, and should get requests from any environment within this data-center. + Zone = 0, /// This Service is in Environment scope, and should get requests only from other services in same environment and same data-center. - Environment + Environment = 1, } } \ No newline at end of file diff --git a/Gigya.Microdot.ServiceDiscovery/ConfigDiscoverySource.cs b/Gigya.Microdot.ServiceDiscovery/ConfigDiscoverySource.cs index 931c34ec..3f2c1ba3 100644 --- a/Gigya.Microdot.ServiceDiscovery/ConfigDiscoverySource.cs +++ b/Gigya.Microdot.ServiceDiscovery/ConfigDiscoverySource.cs @@ -43,7 +43,7 @@ public class ConfigDiscoverySource : ServiceDiscoverySourceBase private string ConfigPath => $"Discovery.{Deployment}"; - public ConfigDiscoverySource(ServiceDeployment deployment, Func getConfig, ILog log) : base(deployment.ServiceName) + public ConfigDiscoverySource(DeploymentIdentifier deployment, Func getConfig, ILog log) : base(deployment.ServiceName) { _serviceDiscoveryConfig = getConfig().Services[deployment.ServiceName]; Log = log; diff --git a/Gigya.Microdot.ServiceDiscovery/ConsulClient.cs b/Gigya.Microdot.ServiceDiscovery/ConsulClient.cs index faae1b81..372b14ab 100644 --- a/Gigya.Microdot.ServiceDiscovery/ConsulClient.cs +++ b/Gigya.Microdot.ServiceDiscovery/ConsulClient.cs @@ -61,7 +61,7 @@ public class ConsulClient : IConsulClient public Uri ConsulAddress { get; } - private string DataCenter { get; } + private string Zone { get; } private CancellationTokenSource ShutdownToken { get; } @@ -78,9 +78,11 @@ public class ConsulClient : IConsulClient private bool _disposed; private int _initialized = 0; + private readonly IDisposable _healthCheck; + private Func _getHealthStatus; public ConsulClient(string serviceName, Func getConfig, - ISourceBlock configChanged, IEnvironmentVariableProvider environmentVariableProvider, + ISourceBlock configChanged, IEnvironment environment, ILog log, IDateTime dateTime, Func getAggregatedHealthStatus) { _serviceName = serviceName; @@ -89,18 +91,20 @@ public ConsulClient(string serviceName, Func getConfig, GetConfig = getConfig; _dateTime = dateTime; Log = log; - DataCenter = environmentVariableProvider.DataCenter; + Zone = environment.Zone; _waitForConfigChange = new TaskCompletionSource(); configChanged.LinkTo(new ActionBlock(ConfigChanged)); - var address = environmentVariableProvider.ConsulAddress ?? $"{CurrentApplicationInfo.HostName}:8500"; + var address = environment.ConsulAddress ?? $"{CurrentApplicationInfo.HostName}:8500"; ConsulAddress = new Uri($"http://{address}"); + _httpClient = new HttpClient { BaseAddress = ConsulAddress, Timeout = TimeSpan.FromMinutes(100) }; // timeout will be implemented using cancellationToken when calling httpClient _aggregatedHealthStatus = getAggregatedHealthStatus("ConsulClient"); _resultChanged = new BufferBlock(); _initializedVersion = new TaskCompletionSource(); ShutdownToken = new CancellationTokenSource(); + _healthCheck = _aggregatedHealthStatus.RegisterCheck(_serviceNameOrigin, ()=>_getHealthStatus()); } public Task Init() @@ -197,8 +201,7 @@ private Task ConfigChanged(ConsulConfig c) private async Task LoadServiceVersion() { var config = GetConfig(); - var maxSecondsToWaitForResponse = Math.Max(0, config.HttpTimeout.TotalSeconds - 2); - var urlCommand = $"v1/kv/service/{_serviceName}?dc={DataCenter}&index={_versionModifyIndex}&wait={maxSecondsToWaitForResponse}s"; + var urlCommand = $"v1/kv/service/{_serviceName}?dc={Zone}&index={_versionModifyIndex}&wait={config.HttpTimeout.TotalSeconds}s"; var response = await CallConsul(urlCommand, ShutdownToken.Token).ConfigureAwait(false); if (response.ModifyIndex.HasValue) @@ -209,10 +212,12 @@ private async Task LoadServiceVersion() else if (response.Success) { var keyValue = TryDeserialize(response.ResponseContent); - var version = keyValue?.SingleOrDefault()?.TryDecodeValue()?.Version; - - if (version != null) + try { + var version = keyValue?.SingleOrDefault()?.DecodeValue()?.Version; + if (version==null) + throw new EnvironmentException("Consul key-value response not contains Version"); + lock (_setResultLocker) { _activeVersion = version; @@ -220,9 +225,9 @@ private async Task LoadServiceVersion() ForceReloadEndpointsByHealth(); } } - else + catch(Exception ex) { - var exception = new EnvironmentException("Cannot extract service's active version from Consul response"); + var exception = new EnvironmentException("Cannot extract service's active version from Consul response", innerException: ex); SetErrorResult(urlCommand, exception, null, response.ResponseContent); response.Error = exception; } @@ -239,8 +244,7 @@ private Task ReloadServiceVersion() private async Task SearchServiceInAllKeys() { var config = GetConfig(); - var maxSecondsToWaitForResponse = Math.Max(0, config.HttpTimeout.TotalSeconds - 2); - var urlCommand = $"v1/kv/service?dc={DataCenter}&keys&index={_allKeysModifyIndex}&wait={maxSecondsToWaitForResponse}s"; + var urlCommand = $"v1/kv/service?dc={Zone}&keys&index={_allKeysModifyIndex}&wait={config.HttpTimeout.TotalSeconds}s"; var response = await CallConsul(urlCommand, ShutdownToken.Token).ConfigureAwait(false); if (response.ModifyIndex.HasValue) @@ -283,8 +287,7 @@ private async Task LoadEndpointsByHealth() return new ConsulResponse { IsDeploymentDefined = false }; var config = GetConfig(); - var maxSecondsToWaitForResponse = Math.Max(0, config.HttpTimeout.TotalSeconds - 2); - var urlCommand = $"v1/health/service/{_serviceName}?dc={DataCenter}&passing&index={_endpointsModifyIndex}&wait={maxSecondsToWaitForResponse}s"; + var urlCommand = $"v1/health/service/{_serviceName}?dc={Zone}&passing&index={_endpointsModifyIndex}&wait={config.HttpTimeout.TotalSeconds}s"; var response = await CallConsul(urlCommand, _loadEndpointsByHealthCancellationTokenSource.Token).ConfigureAwait(false); if (response.ModifyIndex.HasValue) @@ -324,7 +327,7 @@ private void ForceReloadEndpointsByHealth() private async Task LoadEndpointsByQuery() { - var consulQuery = $"v1/query/{_serviceName}/execute?dc={DataCenter}"; + var consulQuery = $"v1/query/{_serviceName}/execute?dc={Zone}"; var response = await CallConsul(consulQuery, ShutdownToken.Token).ConfigureAwait(false); if (response.Success) @@ -350,7 +353,7 @@ private async Task LoadEndpointsByQuery() } private async Task CallConsul(string urlCommand, CancellationToken cancellationToken) - { + { var timeout = GetConfig().HttpTimeout; ulong? modifyIndex = 0; string requestLog = string.Empty; @@ -359,9 +362,6 @@ private async Task CallConsul(string urlCommand, CancellationTok try { - if (_httpClient == null) - _httpClient = new HttpClient { BaseAddress = ConsulAddress }; - requestLog = _httpClient.BaseAddress + urlCommand; using (var timeoutcancellationToken = new CancellationTokenSource(timeout)) using (var cancellationSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutcancellationToken.Token)) @@ -377,7 +377,7 @@ private async Task CallConsul(string urlCommand, CancellationTok unencrypted: new Tags { {"ConsulAddress", ConsulAddress.ToString()}, - {"ServiceDeployment", _serviceName}, + {"ServiceName", _serviceName}, {"ConsulQuery", urlCommand}, {"ResponseCode", statusCode.ToString()}, {"Content", responseContent} @@ -417,7 +417,7 @@ private async Task CallConsul(string urlCommand, CancellationTok modifyIndex = consulIndexValue; return modifyIndex; } - + protected T TryDeserialize(string response) { if (response == null) @@ -443,7 +443,7 @@ internal void SetErrorResult(string requestLog, Exception ex, HttpStatusCode? re Content = responseContent }); - _aggregatedHealthStatus.RegisterCheck(_serviceNameOrigin, () => HealthCheckResult.Unhealthy($"{_serviceName} - Consul error: " + ex.Message)); + _getHealthStatus = ()=>HealthCheckResult.Unhealthy($"Consul error: " + ex.Message); if (Result != null && Result.Error == null) return; @@ -477,8 +477,7 @@ internal void SetServiceMissingResult(string requestLog, string responseContent) IsQueryDefined = false }; - _aggregatedHealthStatus.RegisterCheck(_serviceNameOrigin, - () => HealthCheckResult.Healthy($"{_serviceNameOrigin} - Service doesn't exist on Consul")); + _getHealthStatus = ()=>HealthCheckResult.Healthy($"Service doesn't exist on Consul"); } } @@ -513,13 +512,13 @@ private void SetResult(ServiceEntry[] nodes, string requestLog, string responseC } - _aggregatedHealthStatus.RegisterCheck(_serviceNameOrigin, () => + _getHealthStatus = () => { if (_serviceName == _serviceNameOrigin) - return HealthCheckResult.Healthy($"{_serviceNameOrigin} - {healthMessage}"); + return HealthCheckResult.Healthy(healthMessage); else - return HealthCheckResult.Healthy($"{_serviceNameOrigin} - Service exists on Consul, but with different casing: '{_serviceName}'. {healthMessage}"); - }); + return HealthCheckResult.Healthy($"Service exists on Consul, but with different casing: '{_serviceName}'. {healthMessage}"); + }; Result = new EndPointsResult { @@ -552,7 +551,7 @@ public void Dispose() _loadEndpointsByHealthCancellationTokenSource?.Cancel(); _waitForConfigChange.TrySetCanceled(); _initializedVersion.TrySetCanceled(); - _aggregatedHealthStatus.RemoveCheck(_serviceNameOrigin); + _healthCheck.Dispose(); } @@ -570,117 +569,4 @@ private class ConsulResponse } } - public class ConsulQueryExecuteResponse - { - public string Service { get; set; } - - public ServiceEntry[] Nodes { get; set; } - - public QueryDNSOptions DNS { get; set; } - - public string Datacenter { get; set; } - - public int Failovers { get; set; } - } - - public class ServiceEntry - { - public Node Node { get; set; } - - public AgentService Service { get; set; } - - public HealthCheck[] Checks { get; set; } - } - - public class Node - { - [JsonProperty(PropertyName = "Node")] - public string Name { get; set; } - - public string Address { get; set; } - - public ulong ModifyIndex { get; set; } - - public Dictionary TaggedAddresses { get; set; } - } - - public class AgentService - { - public string ID { get; set; } - - public string Service { get; set; } - - public string[] Tags { get; set; } - - public int Port { get; set; } - - public string Address { get; set; } - - public bool EnableTagOverride { get; set; } - } - - public class HealthCheck - { - public string Node { get; set; } - - public string CheckID { get; set; } - - public string Name { get; set; } - - public string Status { get; set; } - - public string Notes { get; set; } - - public string Output { get; set; } - - public string ServiceID { get; set; } - - public string ServiceName { get; set; } - } - - public class QueryDNSOptions - { - public string TTL { get; set; } - } - - public class KeyValueResponse - { - public int LockIndex { get; set; } - public string Key { get; set; } - public int Flags { get; set; } - public string Value { get; set; } - public ulong CreateIndex { get; set; } - public ulong ModifyIndex { get; set; } - - public ServiceKeyValue TryDecodeValue() - { - if (Value == null) - return null; - - try - { - var serialized = Encoding.UTF8.GetString(Convert.FromBase64String(Value)); - return JsonConvert.DeserializeObject(serialized); - } - catch - { - return null; - } - } - } - - public class ServiceKeyValue - { - [JsonProperty("basePort")] - public int BasePort { get; set; } - - [JsonProperty("dc")] - public string DataCenter { get; set; } - - [JsonProperty("env")] - public string Environment { get; set; } - - [JsonProperty("version")] - public string Version { get; set; } - } } \ No newline at end of file diff --git a/Gigya.Microdot.ServiceDiscovery/ConsulContracts.cs b/Gigya.Microdot.ServiceDiscovery/ConsulContracts.cs new file mode 100644 index 00000000..0d79d9b2 --- /dev/null +++ b/Gigya.Microdot.ServiceDiscovery/ConsulContracts.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Gigya.Microdot.ServiceDiscovery.Rewrite; +using Newtonsoft.Json; + +namespace Gigya.Microdot.ServiceDiscovery +{ + public class ConsulQueryExecuteResponse + { + public ServiceEntry[] Nodes { get; set; } + } + + public class ServiceEntry + { + public NodeEntry Node { get; set; } + + public AgentService Service { get; set; } + + } + + public class NodeEntry + { + [JsonProperty(PropertyName = "Node")] + public string Name { get; set; } + } + + public class AgentService + { + public string[] Tags { get; set; } + + public int Port { get; set; } + + } + + + public class KeyValueResponse + { + public string Value { get; set; } + + public T DecodeValue() where T : class + { + var serialized = Encoding.UTF8.GetString(Convert.FromBase64String(Value)); + return JsonConvert.DeserializeObject(serialized); + } + } + + public class ServiceKeyValue + { + [JsonProperty("version")] + public string Version { get; set; } + } +} diff --git a/Gigya.Microdot.ServiceDiscovery/ConsulDiscoverySource.cs b/Gigya.Microdot.ServiceDiscovery/ConsulDiscoverySource.cs index 7b1d3c37..a0c30eca 100644 --- a/Gigya.Microdot.ServiceDiscovery/ConsulDiscoverySource.cs +++ b/Gigya.Microdot.ServiceDiscovery/ConsulDiscoverySource.cs @@ -30,6 +30,7 @@ using Gigya.Microdot.Interfaces.SystemWrappers; using Gigya.Microdot.ServiceDiscovery.Config; using Gigya.Microdot.ServiceDiscovery.HostManagement; +using Gigya.Microdot.ServiceDiscovery.Rewrite; namespace Gigya.Microdot.ServiceDiscovery { @@ -61,11 +62,11 @@ public class ConsulDiscoverySource : ServiceDiscoverySourceBase private bool _disposed; - public ConsulDiscoverySource(ServiceDeployment serviceDeployment, + public ConsulDiscoverySource(DeploymentIdentifier deploymentIdentifier, IDateTime dateTime, Func getConfig, Func getConsulClient, ILog log) - : base(GetDeploymentName(serviceDeployment, getConfig().Services[serviceDeployment.ServiceName])) + : base(GetDeploymentName(deploymentIdentifier, getConfig().Services[deploymentIdentifier.ServiceName])) { DateTime = dateTime; @@ -194,13 +195,14 @@ public override void ShutDown() _disposed = true; } - public static string GetDeploymentName(ServiceDeployment serviceDeployment, ServiceDiscoveryConfig serviceDiscoverySettings) + public static string GetDeploymentName(DeploymentIdentifier deploymentIdentifier, ServiceDiscoveryConfig serviceDiscoverySettings) { - if (serviceDiscoverySettings.Scope == ServiceScope.DataCenter) + if (serviceDiscoverySettings.Scope == ServiceScope.Zone) { - return serviceDeployment.ServiceName; + return deploymentIdentifier.ServiceName; } - return $"{serviceDeployment.ServiceName}-{serviceDeployment.DeploymentEnvironment}"; + + return deploymentIdentifier.GetConsulServiceName(); } } diff --git a/Gigya.Microdot.ServiceDiscovery/DeploymentIdentifier.cs b/Gigya.Microdot.ServiceDiscovery/DeploymentIdentifier.cs new file mode 100644 index 00000000..a187cd8f --- /dev/null +++ b/Gigya.Microdot.ServiceDiscovery/DeploymentIdentifier.cs @@ -0,0 +1,100 @@ +#region Copyright +// Copyright 2017 Gigya Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +#endregion + +using System; +using Gigya.Microdot.Interfaces.SystemWrappers; + +namespace Gigya.Microdot.ServiceDiscovery +{ + public class DeploymentIdentifier + { + /// The environment (e.g. "prod", "st1") of the service, if it's deployed in a specific environment (and + /// not per the whole zone). Null otherwise. + public string DeploymentEnvironment { get; } = null; + + /// The name of the service (e.g. "AccountsService"). + public string ServiceName { get; } + + /// The zone of the service (e.g. "us1a"). + public string Zone { get; } + + /// + /// Whether this deployment identifier points to a service deployed for a specific environment, or is it deployed for all environments + /// + public bool IsEnvironmentSpecific => DeploymentEnvironment != null; + + /// + /// Create a new identifier for a service which is deployed on current datacenter + /// + public DeploymentIdentifier(string serviceName, string deploymentEnvironment, IEnvironment environment) : this(serviceName, deploymentEnvironment, environment.Zone) { } + + /// + /// Create a new identifier for a service which is deployed on a different datacenter + /// + public DeploymentIdentifier(string serviceName, string deploymentEnvironment, string zone) + { + DeploymentEnvironment = deploymentEnvironment?.ToLower(); + if (serviceName == null || zone == null) + throw new ArgumentNullException(); + ServiceName = serviceName; + Zone = zone; + } + + public override string ToString() + { + var serviceAndEnv = IsEnvironmentSpecific ? $"{ServiceName}-{DeploymentEnvironment}" : ServiceName; + + return $"{serviceAndEnv} ({Zone})"; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + return false; + + if (ReferenceEquals(this, obj)) + return true; + + if (obj is DeploymentIdentifier other) + { + if (Zone != other.Zone) + return false; + + return DeploymentEnvironment == other.DeploymentEnvironment && ServiceName == other.ServiceName; + } + else + return false; + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = ServiceName.GetHashCode(); + hashCode = (hashCode * 397) ^ (DeploymentEnvironment?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ Zone.GetHashCode(); + return hashCode; + } + } + + } +} \ No newline at end of file diff --git a/Gigya.Microdot.ServiceDiscovery/DiscoverySourceLoader.cs b/Gigya.Microdot.ServiceDiscovery/DiscoverySourceLoader.cs index bf46bb92..f67dd3e6 100644 --- a/Gigya.Microdot.ServiceDiscovery/DiscoverySourceLoader.cs +++ b/Gigya.Microdot.ServiceDiscovery/DiscoverySourceLoader.cs @@ -29,16 +29,16 @@ namespace Gigya.Microdot.ServiceDiscovery { public class DiscoverySourceLoader : IDiscoverySourceLoader { - private readonly Func _getSources; + private readonly Func _getSources; - public DiscoverySourceLoader(Func getSources) + public DiscoverySourceLoader(Func getSources) { _getSources = getSources; } - public IServiceDiscoverySource GetDiscoverySource(ServiceDeployment serviceDeployment, ServiceDiscoveryConfig serviceDiscoveryConfig) + public IServiceDiscoverySource GetDiscoverySource(DeploymentIdentifier deploymentIdentifier, ServiceDiscoveryConfig serviceDiscoveryConfig) { - var source = _getSources(serviceDeployment).FirstOrDefault(f=>f.SourceName.Equals(serviceDiscoveryConfig.Source, StringComparison.InvariantCultureIgnoreCase)); + var source = _getSources(deploymentIdentifier).FirstOrDefault(f=>f.SourceName.Equals(serviceDiscoveryConfig.Source, StringComparison.InvariantCultureIgnoreCase)); if (source==null) throw new ConfigurationException($"Discovery Source '{serviceDiscoveryConfig.Source}' is not supported."); @@ -49,6 +49,6 @@ public IServiceDiscoverySource GetDiscoverySource(ServiceDeployment serviceDeplo public interface IDiscoverySourceLoader { - IServiceDiscoverySource GetDiscoverySource(ServiceDeployment serviceDeployment, ServiceDiscoveryConfig serviceDiscoveryConfig); + IServiceDiscoverySource GetDiscoverySource(DeploymentIdentifier deploymentIdentifier, ServiceDiscoveryConfig serviceDiscoveryConfig); } } \ No newline at end of file diff --git a/Gigya.Microdot.ServiceDiscovery/Gigya.Microdot.ServiceDiscovery.csproj b/Gigya.Microdot.ServiceDiscovery/Gigya.Microdot.ServiceDiscovery.csproj index 47a22302..5f7d3152 100644 --- a/Gigya.Microdot.ServiceDiscovery/Gigya.Microdot.ServiceDiscovery.csproj +++ b/Gigya.Microdot.ServiceDiscovery/Gigya.Microdot.ServiceDiscovery.csproj @@ -21,6 +21,7 @@ prompt 4 bin\Debug\Gigya.Microdot.ServiceDiscovery.xml + MinimumRecommendedRules.ruleset pdbonly @@ -56,20 +57,42 @@ + + + + + + + + + + - + - + + + + + + + + + + + + + @@ -81,6 +104,10 @@ + + {0E3A2422-DD99-4D75-A18C-96329A842742} + Gigya.Microdot.Configuration + {A90D7C71-EC7C-4328-9DB1-D2C3A30727DB} Gigya.Microdot.Interfaces @@ -90,6 +117,10 @@ Gigya.Microdot.SharedLogic + + + + _S5JSd*)6|d*TuiqRNbxMn%bEj8i7o zd?7oL&FLsE&F72coBC<%Tukj5WE6ap;Qeb8$Dof49Yl$WG$reXnivn&6-}^4B>Zzl zNOXN8KBP!W>XW4k=JphvgY`R2c#9G`=Wl{F>-+av-v?xOsbZ#jC47eipT4rGpyJ5! zuKBLHB40MZ*c0QZ)JapjGdE-TYuE@Gt{-%rY>~7(;wM=T3FD(d#ZPiI9kK?=UuOPj z{=n(ax9Q7NC@bQb&jo07rr4rL^7%TQggo3C6?8SdCL;`2^)@$#Op?D&Xs{;gFgWz` zt0X^{SvjowwFgg|)ij#alaF0tG5Ns7@3>f{@W)#R#LNx@Y+RF?RT|^d^xUTCX)3li zPRkG*x>?S`B;XY4hZoY8HhZq09>z{9{P`Q|t`VKlP@L4CeatWqw?3`xbv*@IZod;R z%O9_)Y)Ze$|MLswlI}|^2T55 zk~#hDa@krD=Qyjz@v>8Sd!2tzwah-ouB=*@i zInA(xNH5LmJX9Hz%zeHD?G`P|ej$e*78ug`Q=Hx_hBj13HP{jM;N`1)q@zj`I;cos z((3KE_c~>swYgT)F;8NIFVgC#5)Pr`(AKi z&h%nl_IKxUgPwIs zsrjdeYtF2A9yIR5Wr!|wuQjoC&a6)E?iAj{%73`T@Q>c6pukgc`Qdav0g|q+K8SCd z4rifbYS^3WBTimOo8zt$Awt5Y7Nl`5Qp0oEDf|YVaF)&qgWwvgu|Gj zdzF{FO~Vy&Skeb&U`>WccUT%H>;zIO8G`Zz6Yth!h$lqVF#4-!e~Slg?HsLbFHdwY zHMwbRxO1}dHg89{&f~w+D>Ra4R-@TJ0YgP4c#0~cG*nA~eRAQ4M`?ler;iKMvBCSB z>yuP3BOhv7lw0cVq->Ba#=dQF(#7%{^xyG%;XXOpkilrurlXULdr6}H} z(It4#Z4fw-?FCE`Au^=qrr4>>bl&PP28usP)0Ag9Q{y@u@ASlAxv^5uo<;`IZP8Y^ z&PetFix2DYTlXx(JrEZGGMd^u#)z1lZ@10aF`2&dJDL@f5g3Dc${L+-bIa^gMYp>g z=fc?WcN1)B3#cvID~on$?SQQ_ZC}`XGwhATM#6-d0EqW*H@JxBI|@5P-Wu<0U>~!Y z%mkV9Tk7|TabRmME2rW)$%AkWKnS9oJXXXk^WCFW+@0SnguupWtANr|eFd41Bx} zQ~sr$Ck!RCVFBPu38}lRkEm2Fk9FchLn}GsvA1V`6XKQ3lm94JphXIAe%{s5>VZcF z1YiW{d?YwzdS(_1>Y&+?iEU!Dx_)90?&pJC(MXAr@Z~H~sja$I1=+Hhra2H(vmR^@ z6eTdde;i7Y%E94DbssX3W?KOde1(@+-lp~Qf28-9eFg)e?S#7h1kUWGXHiZMnX{*y z0k_GJ7-?*~$Q)C=8ajU4cEX-h=#H7q%Zp|PM~H*X zD{Iq$-Xt7-%MmHtJQ>rV3QYf-uj~X2ZJiHYRJmvp@PEogzpA2MNWy##1%I2Y*gyn@ zx&Hh}+0lB=6H9k^#~&bVrG5;WY<6YdM(t=DYl+&OfQNV}=m^AADwdtE18_tO#f-<} z`ee6f_vsRpMv&9e3k=mIm0p>B$~0Y<87JF`uugncVv8b&hg!w!2YjWQ6|J+~t~yha zuw1VdJ+EMAXrED@U0FPrc76zjj#52&*>#D!OxLl>EIq%nf*ySaF%sQvV}D%#7xK12 z%BI1u4E5_6Y`F5L&7cwH%C+s}>n@pHo+B@k|HloRof$A>3ddZ`*$Kt@Uu08F(2>VP z&q+^2SkD|1=c;}RR^o{h7CyKz@Xe;nU<5f-1H0c2(+No1^_RHxfSKclo78^4?T3%K zVZeHp!gDYdT)#*Pb@&%!+b0RBk-d9uZn;{;gTsC(QtZb}MI+&n%zpX&U7OwTD^j&V z6h9%P>K}MMaj`#@E!FVQwJQ?8Yu6_l9NZ`NyB_^IFbz3;fWE?Qp`q%HEfKPf_!HmtI(fEtjYv`Q- zC+Mw+*(0Vq_tFd>6YQ>px5Fim$E`e%spU2G3>bGzSEir=%Tg||G*k+e;SNjH#)FS; zH#?2lf&NuOrZg+Z@lu7sjkjW642H=>jx^d8i@I%U*G+g_>iXg|2V;B$re>v-9r`wE zK%A>bvj+mTdp+%%+ga2gCw@!)@kNr~kx8l>quAdv35~k50rb=^fx5OTJFz2dNv%S6HSK7-3gkhD*0 zYmg_AQTV(|5yUa$)@o~Lls_$vNz`$AYQ$j7LOlB9k91RgY`!VnOwDl+TP1nX7<$04VHmM8y0W)> zGUMLqcD`!n<=}INIi6B7!%J=;>&(erm2!CtU;say)0}Bj>Z=w#agQ(*O_l1J#N_%q zsQ5;5LF&)9jBq10K7#h_x971mv7zQ@PYViG=Cvn#Wo<2vNBBMeM}Gd);`vQ};MG^u z#<R*%tDK^muU0f(c1!fQl~zy*10vp<jAf6e(!tVrjv83DI2vBkBr&G_9jG%>>}qX4FGe9K%V_4cKTGKH2{Ldn|M;XA3He}-svhPc88c+7PGkfggrJ3k3$Y~Adx6Db) zIm5x;dc^qpfOn&fNs%V5p=q5p^PbH$aptl_$?`H0$%AwO6!OWE#uq&hW7D%zISZl_Rt1`Ck$RB7$NG$sB8y%kY^IkO~?-w?{oI zHttdO(v`S_h|?&+k2CMwR$V$bT8J%z+06KYI4IVYDxa0d?YF<})+4g8!jZ;|81x+) zR2hgOx!M=#e>ExFa)bs4gOBNB+#BOz zoUgh2q@OQ27H8c{%op*?=ZO(Xoyk*rG{im7zEkX}5y+UtdiytBE!SVWQxUr0lEC*F-RI2_=Uo)IX#k8l%D-L z9c)^+=evLyeu^I)o3PKV3sjw|26K4S>b>)HaC*agw#GdZ>E{8*SAn6r;U4Hv%kTz7 zsl6AJ$5dU_hP5`HsvTlFzdXU7kR6e993^){3!=IH3u=C?A`$Co8RntF?kNdumcm^v z0A1Gk>F*y*vg`2-u)@}7J*j-bk8cqy*dM1ay&8}y?slQZZ+o@GGT>K*T715MO51@A z@NIWSnMy~%Q|-qWVcdtrtKH-Zi#~N03pQSK)npO&w~J1^Z|FKDXzG*!AL14>np|hb(sG9T-8SQHhxR5M&94uX)9BJtKr5}@V&{( z$N-5z+iPh^k+B1KK1^^cs;*rto`w zVy+Pr2`jXcS>J*tDme^z?*&El1WoJcVmx(&ZM$oLt$Y1x>%t_zxl9W#kY-s-@(ik3 ztO|3{LG<7Pfx!Tmx9w(18Ki} zEp%oOGdDZU|K&t}wDmUd`sRp-T5?eEOM+&V|0n7h8=X!qb}bNyB|B!re+V7^!jWT)EAMFW zs6I!$`7Qqaa!O9^Y zLL@PV#^05gJmY{Mws<@=qwEOnSXbwg;52#5k1Kx53m>|k5AwX`tdaHJQXCF2h9;Tg z3a&3vouy9HfrIE8*LlRyw;XNTCT1RjcOY$+;_V8yCm91@<$fmiZWJKxVES`Z+D!6= zBV_S+yq<(v&MJf-baZZU-yq2~!00l`i-_j%0?^}v{o`p7>U-ITsvW^X+ZwfCYUR&} zY#Fx41O)Rh*`B-?wbQMst~r*LY=#PXTWj6A{@eE*i~otn9R*#8TahQ7sQ7zr&Zaov z2)#{3E&IO+2zE)sY z=>R}KJzR$~3Cc(Bq>$eCmZuS*jCs_^hTb;k^#(p-zfkE-gN6we1v{9z0`OVH#7zim zaSiAd6TF8^UxAA6^SNRQ3leFS-<>BjapDq!Vts^FFDhcM$=`(yxoUfFSpc~#@bSfQ zXfNNp(W0)23`q`n1gKWDq<4Rce}C~G)aOrB)bnWI$-l4vJ*+9-OMUn9$mH_qDGcyC z{duDSf$-mASO^9F<^rKe2zKw6b8J?`zGoCy43e&z^ou}V@ul9^FD|exn>&G*ICJJJ zc=cxzAx!;byK(1m=kj8Q1TF%MWPlsSdGraePiYknjWkd51xbex z^ZhOl622U5<*IU@s0{i*td*yn)$xO6^Tno_grZCvbB4U@J>~~O8J|Bd|3(wSSI-`m zy)nm(;qF^ab}sj_mu>dOqYjSz{I4y1`TP&*4%C(NoB!n4U%mKGzMcBpe{%0p^8d!w z3FH|5jj_L{fAnv>{pZ6wz!>jTft zwA18%{};6kr3?+=9K-w_rTE>ve3+uOVva~ASnq^sIH zI+iS<@Fz>rG492B?6lGBk3!wg8?Xl7nrPA{Hk0V#<7PSIcWB!vnz z>*}m*1sF{T2wthFHfEna8$VN*Lumm*-2w)F=X?F>`2JA|wT%V6->O$nInxS6S4u|e zZOYIYso}4^as={Pc&cijaJ>pp9|#E2tn-nk{>HLbS*TJ0^Tn$?r$Nnko`26p?tyW5 zAF998sa&N?Y7kfYFbVh(5fBXFqc#r&8#2yrIHv$J(a$^X|ITtjy;gQI6E%D^M)!VH z8kosczZ+CNNlL#;MiA0~{NQi=x5TPMSH)$;4*aV)y>xe=ynp1pe^uURRgr&{_s(M^ zYU@Da?*b`-0$-C;d;RMjzQdQIDy`k|v>PO^Yk?{9J=d#k;DFV?Yw5Lp7iP%nzusGS zPiXz+7I1Np$tQZ%SOnlT_c-zF&By{tg4wHyw;^X#|1RNRaz=&(()H?zpKAK~G}95K zLO(7D+#dr%ul?|}W~DxNbaqzHQePYE`+Js^w8UIVfe^ov;_HEiuL6`qKTl#hWppfvVl(5@BvsKqOS1k~o~i?4y&1zt=|Im) zyLr_DjH|$bfq!?F_py9letZqhd_c>^J_A0K{O&Zd|L6;8h6gOZp+GvI5~#_q(!B+G zB*TByvQU!fPYR=XiVc#VE>|s}<@xo^zdF&N6!WX>&FCvxs3mcG@6e=8wUm*;84~`` z@eTs#>sJR&9SH><2t}L_1bn>M-nme{Jce9)0yha35b^R1af-PE4NqU?UjQdi$;BMj z0taeY8^&ZZZR}Xo)YdX8+_-VW+VD}D_2oYI-DRXZRAw68 z0FQFTDKcJp*cL9T@{*}=KkRKStEY8o-lFU|sWr@b;WH*Z~K5XtN>b65Kkk>!g|A!ZN z|GT+vLwNH#fwxz0uHy<3BNCZStH6~gck%9Zv$50PpT&F6G0@ZiOr1?h+_xfUdP(=$ z0(llAf!|3TatWc^&#|%YCrii~+aQ*bgvfgs7>$_+mGud{%o63JFCi}2$>b=ioZl@( ziKo>0sL(fiU3zqvB8D_884{H=v!4$x#FeUrG@f&T5c+AOBb|9{_i@&BXCEok8w%F{ zeBf%qo@)Dvmyw6I&iPO4Uvl1O)duPUsGuD zE@fN!x% zJcEaJbBN0^L%d}&WsiRXkPhI#4UdxiLYt67!=|iY~f5b_n(eeOje(leW@y4@9 zI`0fy8bnn>>Thfi?jU6Ab6U5fs(nxNcnyZX%%k>qrhIZch70lt9k}*n^-t4jkSVl^ zR&10MzNr4@7@b5dHhy@!VTNns{35*B=e&29)|0Z)htW6y)~={cH=7=B1kduHE{wYy zlydNxbB0Yg4d(U3cx+=orFUxZi}$CLw5Q{zf89}pUhoe;c~;LajL1|z`MJr+EAU*` z&i1lOD9-y>*_^~_Q`}||7;D|%!sAc_s&1nGx|36;(!b8UY|%)f4%uxM3pX))js}>r z57>OCi3EFS-Z!!-EFKW=kSzbIJQeW4UW(KCk?8K_8KIi*m0MkEt@3RW-7!`7jyijeU+o@}dKL|~QpCwAZ3{ZJz?_Ff7IN09=K$5|%VP)f9G ztBr-JYDZ2w%S$iz6A`!@Gmj&>BW^%7iaGIBufaa`Wm&*m^Yu=@+;8k)*=-MJ1Q}2d zp2w)l#dw7%A-E3cXXB8uasxd#Kc&Yt`}5~>;io=@+&RL+lj!nGOAEl(@Pve_c{cqj;Jphzl~xKlB?KB0QCx`|3Ye%-m_${ zX?1l3Lo?voUiy2ZsZ4U$xP81N zE=Ey5=8kWT4oW+)HrKXi#Wj_VXUu-^4%;aMo@rLl6BCp2!gM7eKz-F;_|6V6shl`t zoNTr|){B)~tVZ12dZ#Zsa+{kad}cVE=HhcQIIYwwENn`KHR$nUb%wk`REGg^Tkgow z6s0FAw7Y9&2P3xV@5V0@xhN?$8>tSU=BdfRnD9!{Gxp%@Yv(ujyy77A9h_vn*v96L zR^Jfsk!JYxVQ+ekot0E*dLH47hLMC^)nkUnWho3IYK#|VU(#0_t9eG<{tn*h>mCdZ z@u>H4PWR-;7QKBz5i_^cBl%j$p*cm%Wf^Z&xpbJKW0^bBnAc?(X55g)Zk|zlYwU8r*BfIj>X6G{<`-rxx26Vpw=kqPi z1w_f`nDLm^ixewTM!_)33~v+P%BYdDJtdb@s)0|8vlmV~bWq+-#`n33cE%8{xBTZ> z#V#|J3{Q4H^sR?<6iZ~(80?Mc!;{8)ds9XxKYFrX(UZ*=T)yv)S@c)fwVqiYyW( zU#tzn%M#*y$yQu(0v@=UV3k{g&BkMjFG{2fYf!8+UuPs*q(AYfs79_8;OoNI1@JB& zM)j?wlvAk*=;ITC2T``JuF4pUz^bOj$e8eK9+VXp3BRA~dRd}*x{%Ybe0=sKtn*29 zWOty2Okpu9TuCvfiVaDLqnl^Ulq~o)k(4Lf8hler#6Uxhllg%7K`Iqh@8QV`RfL|T z8@%8sp41U|ep`oFh|kk^ndI}6@Mw10id1I<5Y$m-a^x2UmQ{i#(Ha+N%vr`_P~3xw z-Z)$NI*v-U?)N55Py4z8vvx~A{b(LWQak(rW_>89mA&2S?XaWfI3Di02_7U%4YO_O zl13zr?@tdd#cS(0J7~5AB#zkk*)_CdOI_xF3D4*tYIP%BL5L4uFKqXJ1V=YrYR4NJ zjBh}}L9;0giH2N>V$JVcv&~bet4>Q5!#e1}y}5H2jWJ-Ivyt)_{5in9COqEFZ7U&K z)Ymn`2~RRc^-cv>2K`_Q`4A`O9}x6{CsM7m1Y^+0qe^SwNja~5;G;zK$`cdSDc#8o zC6sl?8mpLygL%bTbZm!RpueBCP9!8SWOn8)@`fa=U{GKcngI#Hu3KH&^Q$(D@w@6bidnjeSLfNJM$Y*)Ay(P2db#GOj@V`!2ZQ1DWFC(;beu7d6(4`Ziw z+3a8ZsH3TmG<13!a0f887S~UnQYv2S_vyKhqpG|x z4y?p}$C8Lqpt^Rlybq)G@(FTcxvo{to&iLU}41< zw=Mm_QVN`j!!l}IJH5^!P-5e43G+B;I-1caW~1ZX9de^0Chti_kLBTX!GNsd7_ygPE407|jy7>6RN01M z8F`%gmh+Oa#0<$;Ygg1$@{XZ&y^u#nQkAu;6t1krF}K*P&YcTyvNHT;&P}!x@2Bxr zOi4va15{ldH@>T!)28Mo@m2jNA%vMHUnn-yJq_f?hmAC5BB}|5fjr}t7~_-}$LJ6_ z(bioK8KCjS*e=ColE9I>w-ly8YTbcYn(8Th;L#i1Z?`y*7wL=|+EKezu^u(^$1ZH)}D!iis~=OYKFZ}RsHhYj-`jE425T4wG1k%{96iv$_$5| zo&aTE-)HNO?RQ&o7-uRbWm6<`Rb(=@wK)%VQcbN|dfV@R5E+w4GGoK94O@XX#_FKjZQXcUb;=vArc7l|Q>5()*Xp)4KaD>#aPZbq<=#j|-*DG)+`Z}$> z2+Hh)%nZ9vz`#~-xlJ4=$(d@4t%Z98+~1No(Q@6;hVtciabr+gj$>?W%bLI!Qawh8 zL_}NPuL>%HtRDK=R;@p*rR=q%cRA6^)zP%t3wo4CraK^7(D=AB*i{%_ko&pfJY(3| z;7-tDXPD=`=tEnEO6)D0GGi_~mf9Q@x3_PC1stKcxPEkK5VHW_h3C(jwd&QY1fG>= zx%6!PY5cmTF@CJC#GNz0 zJDWiEcGkRjjP@r5;(IvHIX3AHjal>2cqYX2V?(+Wr?w5J)GcejYnv$)w#WQP+_v@n zU5c5CoY?o?i|oV18t}FQ@dy9Z6yI)6`hjOx)FDpzZd;(iRm8FM4j$Y70FLosyyqB@ zc>Z`bs;zQ0vzfQ1pAaA7@{7l;Pt_b|#cZ1@I;qDqp!$`$_>1LFeGqIuFnfj5+|?S= zksRzfm+-4(&mPO~DE!mKe5sPG*{4+dfYu`@ct3DPeWM&W6Wb(kxbqU1*;%WMd=j42 zY>%v7O`N&qoAECeKpFiLH1bC52N?R6sIPOgm4>RykUV_eKl`&k;RnIkTaHYLNFn}G zQK5X!8yw+f=)*`$?r*`A@hNu=8o?8aw!ipt46u-mf+lk)C-N+skd2j5DUq~xI<-Cl zIpXSesi=8s_`1+ydv^s#S5cX)?!=e1a3V09T;myGP1Z76^=5SQuCFfMi7l(EFy|}{ z!XX>k0G^?V((Ws@uRrxo|IJR$!aC_*%7(jaOx7E+GOJ$)m)Naze2mHap(D-HpCa4g z$T`SvI@M&x@+u2q@JY8qzD1KNwD$kEj{t&foUEAEW z3$|$eMJ3B5%Q?u=q^e)Kx&&zjfwfUt1KB|&n~ zeKU>ZA3j6iGOob$AX-0{2L=*DpVX}G(Nr-@|gO{CH?KhjvLwP(hpyjTaa$Br9 z{Z^@%@-e7cerzNecNWr>VDS-=@@TMyr*oe5?KzKYst?P??)BUhJ#BaEPi5ohyCjt1{Z*7VOMGx=gh!}NFtY_b z_f@&Ph;{G05g!@rE5Ku0|1nO*QaL&H&D^KDi@?l(8DJL;Q4SexGNr zJ!9qw%sTpL+T;>U5_tKBdsX*{sj6h-8%PVa7@KI9e>!}q3O%y8xF_ z%3Q+Pc1s7a^w?p%e}qj%uDygoSk2Fxk3&cjr|fEtrT2LH4m>qkDN^WitAC$ zclGSzy47^};23YU!|=};zaxAA^JIoodjfM9WQMkH;iW=v{v+t|iTzcyvEiMTQ{w~J zX0Jmcwb@UvCo=9G0S-RDC+yUOrdCUke5}bKbivtDOE+=_IFm%n8INw=7T~v`dB6y& zZgQ`qz$ln6c_zHI<-efo$o5ZgWf<*Dkv9O*OfcoiD5LB^U3F8rSqieUTJo>m!X*px7)+)9DfE zR~<<)pCT2lHfM8}x8#x;I?=sg{fhcOb%Y)(oe`~bF*;~;+$-15uN4lIQWN9|yUNx( z-QS3_+52xNlPOn`*XWFE(DFU!GLGIAg{HMd3t zj3R)_r*l3Q~p2psd%u)mt$PKKucgs62X!!iJLid^Ht%emk&!kIk~{=J0O) zSa8$@1)IGY)B?h<;4#h1HcR>xg=if0r5QYOM0lTJ z=$NccR<8Sz^7h`682`l0oHNqU46T0mbcNr2z1b;>#?u$V&81ISrg=5-55KX2aCX7z zGHiMd-%ifzX|FDJ-4#7Qs}s9+3$NKm9xoRV`4(4g`K8nINW@5I`5nL1@RSB!MAuY` zo&Kz%{zjjq_e8`RMUqXGaZwmFA;LaMXQWq~yT!=0BuO#(`rX7%rnGSjcXm$~+1TOl zVe5?ICNJq^rjks?JEHSJKH}VcE1R@q@J690dq)KTT9>az9zmaDmkRD5zrT5nQ2H9{ zuwSs(k;tevaQZC)TBABk*yYGgWKt`V>KN$Yj)P~D(%81+#M`k z?-^y(GOWz*Sc#CxAXb|F!r8aU91a1LdEuHF=ukT46sLo2c6lyhXe76J;VTnUz5tmZ zajG!p=?`}0ik0KVJu{fH(SClzs363WJy}qEQRC&ii1)pd5sVi}4cONZERM5fn65d% z_9ViWrsAQG8Wcm_ll$`>V1gX}D)&wOjUolk%(W+i-?hBSU89gwOaII~tcEnh0`4&C zL;J;O1Rl=gLn(~#-v2$)6|1nC^P${$klVnDpq{YH1KU~cCq8m~mX^Ww)S7!tP&d9X zTCa3+x#%=a;hVR-rqQqj1&#-(quEk>=ywEBizHM-h`VEDv&0S)0^yj?mX#=}lwFRG zGd6}RxYr9nS5lbTBrJ7+_!PrtlP1iOQ^x)@!`OsAGR7{8uS&z=D0fy`9f$youDRm4 zZDmy7*5b_u&A8q|ZbB;LB|L3}as8Pgcb?`r{@tR+DE+ltVwzd3d1X1+782rIaY$0u zdlGe-`-vu1x++?oo4S-SM%$l-2j%sunla;y3Pk8$>2^+aoKL6O@)}x7ld- z3+2S`su`k?M5`{cqHMHHh0|73<$pd@J`&vF0jdE1`gKfGn91cE@_u`Wt(<*fc9;G= zmD!O`gWDU&n8pS^z8|y--|lQmFxKbYK0IzX6*;nTukMd0-duIjb_$Xa?7!6oT}Qr| z;eR1d%BV$1Z@&Q+;_h3HVp%QkXj>~$6L=im4YaVBAWRLSm-v3fNrO~j+Scvo`fxn1 zmUMEyqR8u=ve%ADwO^F-$b{Y&4RvM2SK$GPXTWzQX&YHI)68R3|IUE|ugmCxBN9I8V1uqF%@{eWjtIS*WsxsKQccF}3?QP2}yV^uC!PrvNRY zLE3c~bGQoqXEmYVwcGX%K9v(i295_h9HtTMEPdV*ecDOED$W z7FvjXocrWzW5Lh3?SY6M)O0p2L=UxN1ozAI3S(5T%&k?1FGOuzVAg9>uaiXdPaKA9 zvBdKsrNy&7`nrD+h2ae*pYeHFrYsy_8*C-!#5wtvpo(s_g#0xbx#-pVZQlVuzY30Kp% zs;dirQp>On6?nowucm*FBIbLtD`m%@GxvuglIv2HN`>Nge(gIb zbeVDRmcg=~;rAJovOsv^fdo>W&X^uk;` zitEK1v>Wg0BHY?<~=8@<Y8(k^e@+cyA8&v&-pmy8cY0vj)^sbHjE)_1dt0_kSwWy->Y7v4X3 z{r%qNSqrSNuk9}RKC$)641r2rI@Wi~2{?`NNQej&<>q&OV+wNDp`@r0cfVY_k5ehd z^SqtyQxrfz9NBY!bS~=}6-KN#9m$b8y16P#SR4J>*V~j(+>egxtx6M`k#p^-<|j+s z5Y`W)pb|gBJxr^NP~Et1GG;R${AVst);QT!-=}Y?fizc_OojfJU8Q03v_6Q`_KZt^ zhwQXu9DN_y`I7;~@{9TEv$y!qJ!u40?=Hey_r@aw8wFR_;`)C+xU7@KsZ_O6`B)Zd zzVh9l;BA?VJn4k>dC{>PTY>u6?RzoRwVukHnZRNj4|R6i(l-B6YV**5R%C@` zp;J=b#L*v?Z2rNr(~4fSRtzhQdQ~w0m{ij09XnPtujf#E#Q~}s$8!S)eh#h_+N$O* zPZ91Q5^KA-82f}Bn}hw@i;04nKA*NMwn70?YkEO{O<8g#W@S{7QnN%)a~rFD@@(0K zMxXLuJG+dLr8(}D?DR;7QYHG@PG%J^kwjhENgX&tk0`b7!b;vBT)bLd?#btz2;)>P zbhBO)A~(eTmL&|@xIu7yWhS!Sk*n5CwC;lMIl9SMC=HTB4Lr#Nd&f0Mn&0C~L?0X~ z<>kH39xlg(-g{e|Y;|<=kf^rW+)c&evgxdDO=Eoc<4O8z3>-xXVO##n=K9QU4*PdJhLICgIi%bpptqAtrlkO|@ zv%b*=)3@y3ynZ{R_`bP`-s4e>gPudSP7TA{O!Iq;+|HewE8p%7La2wmZH(=@yMZQZ z(F~Sn&p&dsg}zg=)uOj9?;mE1(XKIEW*(vfRbR$Gf99|pJEddzAC1Y?2p{?M%QegPV3)p&Ip%I~lF`3Y+*JX!kzZt^a85NV3`oNkKDv z^Yh!N&;)fcS%iLt>8BEJg*|di>r(6~Z-kGJ7gycC{>cfj6Cx>NrvDdrZy6O=(}auS z7Bon33-0dj5Zv9}-Q6X)ySofdfZ!g2ySuwPoZX4tcLOLb3mSJhKh zXUTH=x+PijOe#8v+}a2!7vcb5h}O8)l?s0A7I-@Vl;75rSSDR^EPULBTg~)A6&N3vhiRDr z)+*8?*PRuU>KDz?`Kqx$Ki7AP?}e{^$$&d)-H9#F3Y?#qH~tv`^qcZV4uvJDo7>W0 zBrVre#{a?2J&qVIPoryJn3(c;UVE7lbjGxd_z%s-+$ z>@Jga2u&i0*6Qt`I!70;!|x4Z)$)hKh2rDippU6cW;8W4RGG}q+iWQ^tIdf$H5oi>g+@hZr z(UB-y$)}^V?5rV5W@wW-$fesbr4a7DO8xf|V=hUN?dom6(#~X!cZQ-VWq$86ysyF9 znW1xrz?u=dV1}`Pt_6&5BmImWIa>VA(yH(%K5%-)h4nA9Hjl!PI6Mlo(}Ls`W3gHvn{nQVUB=Ox!bG`RcV{X- zH`X#uZ>$?Qtdv|{YBMEO%?sbA`=upNQ7fO1>l_M+7JLP1vTtntMlB3dHZr7I_l6_a zgcMTdox|LU^!q#GjW~bGK)X}VlNE{w34;zTuw!?KXXS=si~rSSpayF5WypTLfWd`9 zv7#YV)?RDF-aJRWw5_ zS(UdultmRjhkc~z=ddn907mN$>> zB`j1o_hQ<)Yv%fJmftQ%&d)2w{7^;z>LexS<49Fk=VgBwKEAIj(+}NLH|zX%y5Cr* z%0wz0h<#DyHz2lhFn8W|MxZ=d=TwrvKDpJpzP@PwJ>Lk>oGh>UJq3u9q3lXe4LviZ zbUyq!RDKt{gY9gCnd1D1gk52q+lnUDTQmeH7TC&fk6f{heQ{ zz09-<-oiFLUF#yecpK`%BGr8nZ@tX9a+HO}xRy0aS*-Q?*zPl*yQn9vJ!75e^!x|V zuw1SJA06dP1vs*gqSSfUk7_#fhJ(~uw(i7PV3JBLW^_UsrL;*$nmlN+J#7Oh)1>2< zTT-tq$yJTHqe$xNVBS(REjd?d%BI(|Ol$8?X8;9a+w67~U-+wS zR@mqp>01Wv))t&++GLK3v|w+hWfNv`R3~X|*e+P(erVm-Q`GpB4vitOwz?e6&{rMI z#kdK8ro!`2H&qu&VKCkCYFZ^xS zvlQ6jn_=pK{9-(6eV8;FI>GXL$U z==8`QDOas5zrRODFLrbZ(dsV&4+0&j_oV+nwm_$TRc+^oZ^aMLQ{E-OFT$kD^}y#xra$u8V7#xLF<(?Rr<{xsD@!)aGV= z*io^uuJ(|vL2$WqNl_1&u#j0_yk?XL*k$BwdUjGhHZsI)C8N^I44nN~+slfxOvEWe zf_Sgzi5;zGE*si2vlgC>MZ#=`Dll?sf!RRLfV=yA=oV&qt6JORQdRy|{qrt0Ee@OK z_2Hk=TniH1crC3GCZ2`YNgW-0HunBwPhNJrrOE^S@??QLxj(EQO(n%-XTE3}zgVg$J zH~ha=`l>o##*}u%#Lmn3-VB?mm0q;><2TQ1OW-pF(3)~AbTsBoD%hHCORAPy z5aQbA7sjH!W4*=UgB)5jJ{HMGO{Ny7Zw7vzvjOllCk_V!WwvQoJ*zdyT{H1BV`?5H z%*^r)(xE2hi|5xV$c;L1BV&*(j`mOz)m{oCo0a}%2j>8yk4#@Ly)retYh*?luyVbR zRVM8;mFp8`kpQL69j&s>-|psGVEgVaTJ1%1S@m|BGWK4qbGw0fQVuy!7N4^MHhfyn z=AX}CzUkcaPG9JgBGwpxo=?zp)()2Y2ts-l-XWYK4v2iB9{; z`9eE2L23!2mab)``J=-CHOhu8Vfs>JbYc-DQzV^AvO_(0a*WYv^=L87biS-yo~o(t zsGg*C_v(c!jdC()xIZmP+x9J3;C2`x#cCe`6(XjpN?EUq$M6F}@)*|2 zvr%pWp1%ZvH0Mn=WQuYi5MSk5l6K{_ zy=TwvPONIEtmyWxFQJ4^)^bv7ZtyxV!9ywa7k6VWCj#YR!qNW5Rv;d-pI`Ely;4<2@wVeF7#--p_~@{uy03d+{{s;s>mcNW&B4TjS- z>ezhL#)Rp@FcY1u$nqpvin^sihFmrTxxqUs6KiEPMf$rJN7=-yTB>q%S}hC)cna+E zVS8 zd3ni};r8;b<55kyjWQA zl(y3y+T4h)q>;W9X8eRKmeQpjWt-vRRnT!>ls}eW%fx4QLWptYL7&Az%mix+WlGnP zdCWFb#5^fJO6zlg1m@G4Q&}4+nbP_Zd&hvT!h@mA*i=z;H(Ju&|Lb=`uaSbq3}(Mg zC(yR+sFZ<=r_9=wA3}N(4$z)6)1Ty980TQ##CG4}6 zxVl_6fLWJM+2U*w6t5y*YXL%LvyQW;BcS?_*TecKI*XwbHiR~4ZkFP3u5iPW)Lk%s z*k75hTz!~2m3qCHQdCkKim>}x0!_J^r~0^+B}?9|UP_b3;k;uBc~mvFj%8-c z*GP!sv3+Mj-KPYW%jTu?lrFtxCK3m>yT^8qG3}gL{oEhPYk`U>UxthAhso_WmihV!#HKWT-BdPQ1W zaN!+?7k-HZDXGloCG8&jQIoC-=C5!Z?7#ceSUM9NR}a~|uj;{YCTQ>iTBA+&s)A_m z?~bmEg9w7~WpwaWw#%PJSZ&C@#^9Wv9?jkU{q=Q&>?e}iwWjY=)m|l3DcOVHueUZ= zBkeAqj9Q-3y37T!?ZYG2noUt1#Ip_jPYLW|LCI-A#Zj{vi_-wdM3?v)Pf-hfdRZ1!#^!CTh?b6xL zBm6DI%06Ns7x%6WH8X2W@H9=|vze`oV%}Mv1GXyPDy z=dYS*fYy$nWc!m0>9u$&F$e37tC*OYA&i|w(0-5Q%MeHMfEzd{&--64KBKH4%3X4KzR)%j%C3 z#4DX@(jSrPDbn0Ile3M_sPkUx22R2u_gvL&n~nEmmzGQUU#9+p1>nJu|Kn=Y_Fiao zWjUWfh=a{5M$pH3={Md6(rrCI zn07iLD_=OYoHbf$UbLYeqpi8G!?#?SDm>*LC1$8EO};Ap_i}uv#TLudMB4i{p03dc z6w>)_CX=Z|_Rlg4?a@B0xSSmnA`r)g#ntw`Z@}d(-dg+DhK>ly>CIVIooFn-v^Cuw2dM3ZyUG_e zm;8^ifwLE&F|YGnw>bt36SKXBp1-U3is&RZe$7FQer-CHU}DiZl=YGx#0T=|N44x_vHR)_KE)V!mz9}708F!&B2R%N~NO)_Hu>EvL zH{3HNm1W{rC%K2~a}IX|R~R*&L^|ys0;yp#tn)^`3Ss;+9er&NQv)d%YwRq=FAr6) z8P!ci70_WYGABK6R@hwib33Z1)8cK+HnvG2&L{IpXD>J@LZ`4t_d#CR1kUFAZO<&8 zdpQm^$}i;2YDzD_Lu4!cPuE6HwFdjuMllPcACy1D^2aoa}J!rDaSata~*nXb!Vqs9@ZYck7B&o?e7tM z9WJ86;sYgivCL&ZTKMLMnAu)HoQXm&5hsodqoy$Kwp34#d3BiQ!YUVt5_pN5c@(T& zrh)JZPjmuivP?@YN%UyQ$^PsKmodha)YUiPHqQ&S3^r%i+}mYF-itEKPD$rpGSlPO zrYu&QHS(TUzK)E}8-WGfZEQEfl2~6C@%{JroPb|^FLS(Mo#?5dgMg!+wNbkl@pOn#ueGnIK=+T=tEw*n zg~_8PK5usm&p_9(JA#iZ0yCW~r&3Kis>BfrJ&N)v^WiZ@-BxQ3x9tNb-iqTE%Xy1?t(yQaiMq{78Fx8lrnw9pC>5=Hp*$_VHQaJ}#1 zZ3g0DcQrMiG-c>gwdhiJk$`zBf>%5@rd>6Uv{k)SHsW^GIIvaRHyfGfTx`q~|Cqp0 zjZ)`!$!$r9ydzRqCixUL-KJQwrfS=!YJavw{q>$(mwV1pGFektMY6+jop6&>3aS(P!?XV>wVW`U|H#PSkd{5v&z##t6W;w>qQud9}Y&h zu#yX^Xr8gytB1tX?F1DqR6uC_6~lDX#eV*MC*tU4!nCaunBk`fn9R})m}|~Y(asLQ z_wjnuKnLWX7u6SCAG%svTF+sAkI-)~T^}zR-wrxd=uDGF(3B~XHwIJQfh4y7HTGD` zUAHH>lj+%7RxK^IhX$=YainvyG>Fz z=dipVb8PXp)iQzbV?hMqQ=_q>I|=MA)?QRQRr13NI(EVY!K6OP^t=am`{J*tW6}=E z8VhQ@#L+#^m7y8eDjiy&<0G}!B2K`(`RN_KNa5KX{cpdF7d#RGeV#r^fqW@@N^YB{ zX4glx!^h-e(*&NH28R_A0iSZQ;uQbK4L>j*)SceO(#tyuz00P~@!c~k&-TKV8t5$* z&sR0C6)HRh#0`J3?IAor+VY`Fd1I0M@G*+`aVcUyclGy6Sj&EP!V7Ng*p8uZ(~Hg$ zMnm2mc7z3w-6&SLIT`o}!Qw3Ty-`Eou;-l*-47}kPL%ApZTfTpy?x)gdQ7GQ>QmID zzi_vOvN)U?2a?=bT5^>{AB0uvt143e85gr=_pq(ht*~pfTo&%Jh%BDd4WHk^nH*kp z==|uw4*yy9HtMEDJ(vydvb8@cAmd|Ma5pZJj^|& zF$=^y117oG816{wR1R!z*ltB`N7Zw4*sEu9pLZf_ME*8hBCY#9E;h6kH~frlFB>Y| zs@s2=D*T3pU(CT%WXUBuOLb%bW>Kk@L<}a`SiPgB|%#_xb0hRwruCF zNK3&yCdlS+Ae_Q!ElVR`YUFeh9JLZ%W3(IhE^3=U$g!|#FIvmi8EuO#Ogy7^bHP z^y}w`T{&uWb*&nKD3|NX;#%L=7VF4pWwUiB>0+_d44RdcNmrKTW*dRHi?+NLX`qnR z0wQPzS-%efbn;O?U9f&n01wW1NG!aQlYd^9)6t%DGF4dnyHzP0p(N9e?n-6w#{U!F=jb8tu)2HM!wdI-ZzEWMB zmfZ(FL!&|MbOE#akhH7{sPNt?Cwur#Y#{X(K>mJj|3DMmY(75vcq7=l`*CqBBh2z~ zZ?>>Ju$r-&@`2{jWPF^bxAk$OvHH~c+U57MQ39us>wO|;hGG1j7+4C14mUogKH%*z zZz{JS-|x*jsUN>iCFO2?0PjA2`XGOt3EoID*d2H!A}&ChS;*aa=nUUn$zQ?D-h7Vr zdp&r}?X=t>|A6ww>*@t`zKXx%y!S1iz<9qsU2H}Tx2UP_Y$?62U%avTK7V*e0Q~T} z0iC@!8|sr>4$!_kZn~QTQ6mL=UC+;UJ1g;}%+wSpz%n&+e{ne!vG91@MfunQeCTmb z4fA7%AM9hx)rhp9^nm@6`=ie<7VrVr4e$-wcqiyWpv$hs#X$=XKMcBjnfkbciYn9_ zvLn1b`v{!OSn=h4>)Kke!hd@B{_(|^zkYv^{r2eM1LghImnkj$v)Hb5y~IA==lbnZ zpM2HH4Cjx0PYsjphYnN1|LoDf%ao$EJmDwZ{#6Q!7xM%}{#SlDAcPK*rfNu3QX_VdCzKXZOvnE3G6Q)V%OsU{beD+8%)`&q4Ozt}x|MOJrI4J+)!-p4lME^_j1YKt=w}bx@cuiQp1q5;pWSE)`QPJdYia+! zkCKf7`CkL{f%w;b?Ef8OK`hKa&6w-_x3XuLI)l#^Yoh*J837|34X}!-3gCKaG4?m~ zB#1;Li7E|;3=We;1`$xJ7{=g69L$0150p!>4j80xPII)Z#!!9-v%MGAk#y!q!T z4;(5mkzMe%tWiedHZ-(`n(kGG*TtIjDZM6u=G;hM|*a}cxfx$12YRK2yy)q%zh>TR9Po)fBIdQ5o5IRROyz% zWY2l}UAaEy%foaYi20e3bgvoSlF-*_bm+Mzwqw;Qp9L3?aUU>3k5X|>dvBdSjW_tR zT%s&#!kb6iSu2mjSCRrG0vt5+NJjIWv6U7Oe?^jrOBK>^(H90=Zj}?%eue@Cfi%u& z!H$@mJ1-XeT*@pg|JTRoLG7q@vFEcQdo?N&zDqba(0mazb6GeZC=gB*=nN|sG+v8) zUTXCPuVio3cs}0V^GcmloBepq)Shw5&!32K`vl6D*e1;5l=@U3hTtNlSH92+bd*VlHq^jVD74HGN0&JB7Bp6C-K(efCgV+p=?U=X!Y5`epNDng%l)S;dT0^> zJn+%I(*Qhz2Rq_vp)$g3u>xEGcR&;2+K2o!rs>O)WRT?Lt|Yu3TECsr3 z)=C{&eH2bWfaAz@(}Eh?kXd+OPS(gY)5sYL62i3Pi{i3vgD}(eY=H`prA^ieZW$RI z%Fls1_xT%OB8$N0$ML9PS-FRP`_=X3{tE*{&ypBH8Rj7r2`38{93>304_rdx8;t6Y zPEZ*dC~&+$7))f8INQQ+{kO<))RamWE2X|FJ?_uH_btHDGx(O` zDMnyu@Xic$zRN(1zy_FjG=$HK-P<$06RSyUPucCJ#F#NUBV-`LFn}nuO?6_Vci;!K z(($9Vl?W-Yp7;@iUC%w)spsZu(}F4rgyO2lIWbQJH}hy=$Ae> zpWp$6FQ1P1h%+p9%}&(1xMi-mqyq2|g!B=htSSK&NArAD^&j=>{gZjl%IaEzNnmk% znQp3Z!HFPb2yoHOk-f@6fuO~nY%MZP%mPfCHY}5y&E)+k89JHWli6ut>i9t*iI3(C zlLp=X1~75E$h)kj-__@$k3pOfDoh2?ATg1ViZQ%2gq^dasp&s2msB9KI>5h z)77#V0ZhvMIbDtWURjIlkMJuXvd>1wVBjZjgySzNhastp^?!uO*BF_uK_U+56`+BVLLf9Ku!E>cZ)>B!U%n*5FlJ4+U|0);e58>XwgPx}$X==n6b5XCF|PGGMHi$RkVCp)dQo!h@fepB24zc6FN^y)PvZ#cF*v zcVRoVv2@h*6I7NN);f1=-9&)FCGgn7bl-PMeypU7lgr%`qpDV!R#F ze1K(r9FIEkW_0s9%72y~95@)%gc2cI?Gy*j(>x z*v`l8FRv$E@6??iZx5e@QjTB_B?vG;w)-{9zENw z*_If9<`|##hyq6iv1`wsNi-s4A>2f=koA19YGj8Q{jxI6ErH`*UgB(acQ2^fQMjK) zr46|yy;wuYa&1>pi~=tFCj?3$!^xV9a&mwseRI6iQoXH7@>SSS{PaZMF-{%e0Gel!}=%;@lB2xz$?N11{H z92M2`R^{D_ePH%LPlckx=?yN zn_8pQ_(Cv2%+EW-$R;0@4YA8ia(ANjumG)Lv}XKm6VSG@kwqv2j2R#q15me(JWjS; z%aGi1J8|G(47KkoD}95ZAHH5gTJBTx49fL6LYV<#d_Nb$ds^_}h#tycIpqIzRYaHU-LK+JIOs5?B(aDVd{oLMx<%kU038e$07ooR#W z5VrO%kwvEfWR=cIf-_7yZ~j5n5Ag;E5fod-&h}9~&r8edIEdi0?Yh(I3ZVxsntyZe z&!9Xl%XzhBL|aJjn?RrYHQR?!@iL>6Xg^Vpi&I+NtnyzhU&b}TLpA$uQtfzh;fV25 zT?SGDcFM*_PFbG17*0H(+J2C}$TEs+#Ef3)kV&tR_DFhWq{3pTu+)Wm+EIzwY3| zq8~6*QIU}ao=IYM&ba)!up?vS-AhUYEw~q`bKw{)QR9tdo%&`R>^Cf{23Uabg*Nb2v2fwPG6b;D7JXe^1mt!dvF&$vO!oOY-E~vI4QF-71tn(D$T*_F-rbu*!_ON_6>+Qq)N|$RL8h&U3O` zVXj_!ejaJ3W*8#S$!2hPz3$;Zx!C)z+&hHd)i=K_7P3`DYFmk9GKWeFL+_>QBmh!; z_41wxo~WFW(D5#qk&=szx=eU3u@%?xrEI^mQffO7+8MV-3Qn5`UItIh7l2U2@cEZVDKZ#^LJFKLQo)O zPgS!PJ2g~z99+iAv-mhJ`dH9eu;KnrkxEfoMeSo)9UVxOv}kne_o%8C#NV=zWi^H}Btuyu zK<@aCa`KI@=b5j`=Zh?_`85S;NZrjo@uFt3z*nR80a9~CL@JGfdX-EMYr6^p>oAQc zRy~2If0LI?k*vQP^7FKbTeh!kiL+dBRR5eEMd~)`xcKr*hWa$~ z3ACpL>zGkP^-{2D$`W0i${-}N!QstS>3~EOa#MXLUTLKyPO;luwT(EmDP&%$-Rhf! zk`16@Vi&ndi7jonVkLAB7UQ%Tv!fD#Bb~y`0q6bDMOz>fjiX14YxD}2*gE<+vEe8U z#SPtSO#BE$6YBInKdqQo0PP5RkgS`A)?zG~&fXU#E# z0ILYIG>9zDhxg@9cJ7D}gZHQ)D}uucBis=zdh}u3%xyH%?io?jh9o=2ow#>|SW}jM z&`9~f6KJ*B3q(D@YYZKBH>44a$z$Yk!$!Z4j2>x~LLq6L|)48ObePQ0{5@ynP~m;{gkV7Ul8PwT=32Ud7-xu4f*;|W9XP}qG=Q% zL`(IjLrp;@mls{eT{f7*I;m4exVyZ(T4@s7?(ndx!j!|V{yQ9#%&?rKjT$Y@%KR{BFFe({sMWI;j|6` z!A!yYp?rBwqF`Bdi!YRz=?{rIHQBy+(ki*&X#N+|#@&u2X?LJ01Wv5Y1H z85e9p*RS_UB1{SM&|Y3=lBLW>{uwh^yxXz{)Z~oVh>W<=acPq6`P5}gQOu8IZ4uGN zpf+Ax1|Z12@>N#ZSvt2*^Hu85`tb5iK@HkdX2&|PN83H8QHRQI**j>{`K=Eq5_yJt zlg)vxB>|hTOgHF=;l@$2MnAl2DahCu%()}(_1fhliO$yLRei$3(+<|H-Nj@NrB*b; z9LSBT>FZ?weGG?QkazzBfwhDy--6xv(9BjXB#q1sjnz+#BGP-^#7zPLW<|4tCesVZ z+OF=;@;0a#ua!0sjJ2WMR8s%t!o&nEn00pNzfQ-c>3S`Cnto2o&l}&6d`>LLIGER>Rzu%WXWNFkHsJvHk3*Rpozf?L%m?^y3TfFBoVBt8d85(CgvZVlGC5FtB|C7mOi2iDzySzW>5U`kg{2H)zbIP((y!I|N80kAWF* z?7kxM=cVw(XOwZ^<|8c;?~6(Z4wx2eT@!3y3stnEQ|IoM5~TrOG}GDWGhM4d2)`gq z{7LjRLLf)$=ozE>b4HaPvcA7Fh7*nr>Owh}PN<>cL_lmi4KZy7w{)|jY50wd^{f{{2wfUxPqC; z6BnbmswO#qM0iaYP{PtKVi5bWKD4iO;V8_Ddtt+UGnV@oenQJKyEpk_R+6v%-rL|= zbrEc`>8Ta!LWm+B`o0+$(9Qln@F&#E9 z*hN(a3)JTcpIW&?3?6UcUhGrZwX5#l>t}0?HJtCkMIlJEvajUxz02&dnJJt5E#Q(3t3M8Sv&yLyOrw!%&6@{LB79&wD8PZAU$?u7#eL@hsfEtdJ?xf%nxtGo-xUvk zQY~O7pOK=)EY{B)i_tC)ahB-!#_KGVr(V~k4_x8Q@#(`fyJl9$`j;=tsant5kGhH zc7{S2G0`3-IF@a_f_G1TyU(AFUMjbQpYV!mX5vvwT&Dqx>t$SIv?^2$GH*hKKq8*m z^I;4IX4yEC;Uhn~*ZycN4-LETY+)pdER@1P;RI1gAe1DAnq$$m9UIaSA`CR_6A@g< zqr!9MvdKEfYyXaOyb_LXOz||Le(ulQ`wCy7I;a9(eiwj8D~fV`qTC{eVcCZGC?c{> zX>|bMWS%^O?1Om7Mg!|qNa{I{CT|tOmm}=KEjsQa?MANY$WKbREluhW+UywVKj|bj5)0}Pi_H5^wIB8G(< zQC5;+N0|J{zp*qzGc4vA&F;EHNG;I)b6-x@2d}`!i92PSy$@1Y@{}*kf&<(07#JpB zrfARM^9TiH!*u4u22z%#|fj5Fni3Rq3#$E+%F%F zFJp(UtPEVXr$bSlbEz2-AqD`I|HPh=_>R%Rm8{?T>DSYQf(c>bP*p+OJR%`QJtl4$ z1tCO0g$Q?P;=J0-drWkC%F#g}gxOCz5GjLKcP}|sd`|v?Wk!s}IDSKslvAANULOSN zGJDD`oT17%Q4x11*>Q%+j9@~HA*w9AUbpF*X2c2=gvIBG!oLc|LohuNqs{c!`knW$ zb=-HCpJIrzpHu3!GBkxQ=it#wUY>+^QS~_e9Sk%&&iE+!1LB_pM@cd<)SQnjV)S*z zvQ4h9r+_OiV7p&yyC6+H4%x}JkL}e?7$Mq*JGNJ8hE{T`DDk674E!uqXDD9Cu;(&Z z#pn^|;NP-%Fjf$oZkKQL0zfaHQHz~kBAhf)&Gv&ZI|;I7!^^G(#sp|RzaV_MKQ{1f zv0%C&0b#k?pTnsP5~bY`^Mol#%=8-97GwWfJI#fwFbMo*X{1 zw4B@9Zz5&^JL|dzeB%&#%3owB-gbLkFjPs2Qoa~^#s_iqUVEF=;Y$APNV#F-!ED13 zhY(JS5@}+q7#9k@kK$TRm-+4c1_ii2TcRZ#VI;{udR*%sUs0%hXN#yfCctu*Wwu}D zP>gtD(Ft371j}<%7cisan2{gDAOB58nB@G8ov->yq{S1uE?UyMy_KIpEx$D+W8!Hp zQfvG|X+~BCWotdOz3jQs`bcU_Bizf@I1*WCP&2N*^#KatRvj9qja^SZu1g)NNBy6@ zC(SKCyIKSvPY$v!zmj9DO)n{70+)Qw6}qiv!)dSJ=Wkvr5VJ-198s+~157UE;?RCG zz`2`w4%(ok^L~>%{+Cg75cv(Ef<=2Son!B1x zX97l78JM4I;MTz7{&cC(P9hCPcDu|Q9oz~IOUH)rz7?lIbvkieNs(qpwcgf_P)lVs zO+_s(_7~&MxbO6FErvkJwyJ<#@Qo5)S&}6_jx0!R&~Aa`DL^o0%y~14eCyjZtRv>k zkF#-S^RFZvYlEI9vuJLHs!d9wrvoMJz5*RVBbyE=pxp4~U4rYG;tYAsyxCpG6F^Ai zFF0Q74)2(mP_Q)+c;O*|vrQJ9zDk>x19i#^HlZqUp=E^e%j1SBEjI&rdD*V+8aWse z@_N}*uh)GiG~SLYm!{}jGeFjTR-(|~mcHHjQ4;Kmi%|X&&&KCX)2j#b5nBtRa>NBzj7L*1$CZG;ZWrt6 z8&BhIH63S9h|=9}uwpx=JlI*D2(2mYJ-@8P7(2!!-K;ZAH-2h;#HKv+i63WDp1djg zopx$U%G{$?rx|rsQ#qV)I5eDi>zm_?md`fo=S&bd2E==OZETKu*?Zy|YA5art!{l8crZJD)}#MW zV>{-0I4sC@A8@*OF5<E6Zam7BmCc<4O1R03Q8( zBfWA&4>Yt+S}`-xU(%yeymM6wVyEPMuTgK-+THfux?a})WqoaW1*T4p?dWE5r=(FzP`4dUJv81SFwKHfT2B@5OO+I-24Qm z_Qi(`m)T5PC+Fis+`>S!DFUM4x5T4&l>1pWSAyt86>4UK9%ad>5`45O1vJ5ntiZ4J z(TsFd&@kiPd^weQUJ5;D={cj5a)b%NnL!RD0NfmqekW*VUTHE#@dM1{L}m>Xfpy%i zw*a%KoYHmymar%T=(=RHRTPv_EhF+1lUz>G1)dzAtDyL!*92nopJ8RVf2CJ~2ob4h zwUu2lNN4mJ{@If407loT6w*poEQ3-nMq6$a%SL^HPcEt0CGR1E4 zwX(d)5j=mL%|1DU^9lY%crdIR_8bS*0qI{vL}-8Q;wUlGwu5H05yM%Zizja+8M`d6 z>E2%{S(I2mhN-^)=D!>ql9_;cMAJ+)Dl!A}SJ~O*oDUpr?(em5f()LqUE=pdOQ)V1 z9ih^E4*Z3mprJIXamT&ccce)GoQJeRrm zio1zkbyz1LYSj+~xu}e{248!C&Us_o3Qg*fgwN1T5(h1c_nFKV{Rse3eQ%Ql=gd&`GLjYBvVp^Q@NYB2bX{Xi8 zdETZLpu+tEpE(3lK>8Es$g8q!z6q2?$Dagy5+2lX^SHN*Pd*=!MF0fe$g#utXumJ( zErxm0`S6~XRthF$ci_qJaFPX~jq$09B*HBr%#t%8)$RmD@i22Qb1XpWHu&|nlz(93 zb!=lZbK{ll{dD5}G(Qn|TxhHZVj#ZJ>#k~j7iw7}tuu!Cmx| zQ`I60$s860m7z0(NjRkD{YCR6wBZ2kKRcr`6fv@9>JuP!I<~3h5U+RW6zb50w3U@i*-7Z z4B>IwWk3WQ#r{l}yDt2Tw?E6XU?aTt@xOu_)?xZS06{YAF)z_x++TSq1mOwM;AlSb z8UjGB1hK#J=w0@p0&N=7T<|QiXOO`uv2?!>EO%y)n(A|JkJcFaYT>#-pFF>Kz!n5g zCQKn!o>2t%N}`ROk&QS1p&0j|x)M>U~6?!XD7E zE=mb9#nq$|*=8sp#$o?3cW5Xu=z=c{LtNzlE3fpAje>(EtusDgcPZ@?7jg?BLOi;Y zG9)P=&?y_?{MAjTg@QgDG?;jSGL0WG5mE9>0k~jRKcADs?_}{4Jo`7B1rjMHjO=Yg zFcMDZj$b2bKX;Lfy0M2cd4I!jqIH_m6c?D&4`wavS*&%s7(U)zCx#zdW?hHlV}Xuq zPIuTC%dOOM3u8WmGzo#Ju8|jz*Kt>w&*f9V&B~%ndQGifQ z+lCi-h?eHrj>BP7IWDC4EFef!w?%`PBB={-2sw|Pr!bm6ZwT5BoTB+;Hb8dc^ehli zP*Jv@SZj$b-+I+MFCy{)M6BE%id-LzUzn|AKAt`?^8hvM6Z|i@cK4@sV4PXHKzBI= zEK3AnHahnw%hB|pIz0iQ$}stPdNz&i)&-M60`{0~~w0 zxp7XWC=B8}Xf}bXJLCQdL25q$Z|tW60EjuDgThWnu>S2GQ7v3qLZmBb*yD*HA`**^ zgLZfoZ-|LRZ;za-li)#|Q@QD={cGpX#1Z{%;mv!mXgY{=HS?8vB$y#b(2X{(e`F6H zf?}K@Bs@LUWtXzxE*0(1s8gFOa|C4ykOJq{vFZ~IcWb0LTfN#N`RicF6cS5F!*5C$Fx1l7>aU&; z)nMNu9^WfDn?wS?rhBUeA_AH)tN*lri`&(O}rs>-@)_-3adNpQ%%&L1!K z232VRRjE!ul*Nz?GLa;bGGm97Fc>FB!p8^Ic9k&fKv__B}BAhty z1WH5!j2;Md`z#=slP=_Np@cBVm<&KdX8+!WTyIbddDBvP3sy5+Fe=+kblB(;hp0Lk}-OjN6zX{QBq)u;eD_ z-8Vquz6hfrJHIXO5C%t^d69P})Zh`Nx$F*zofl(Mf&wFU=JkKIz-YN9w=|)GtE1rp z*_B2K3AXOw8!7GiGf|G{bIiR}Q!c^d6~U!=7(4oq^?B;JEwivGVm1M3*aLgJqi>CRb2KfN_MH6Eb|eP6DF4QbNd<$V&thuMg2kNxo}f z-k)fqI(EhyD(e2C)@zGAwgA=gGP}8eEaVtAjh7uHT>G(f`u$f9dr{Mkdz}YN(YQg% z_p-qpqe@ty>4`+cpR8c0zexO|641%C(cxw0d%(S+VRTZb5zz;sPpru}+5an06dA+usI1^?$PgNb}?#g{kewZDD zA4`0+Jd()PUR5Es?OFU?mh^XeB;x7a(7oO3e0hP^C2V2q|8VveP;vF#0w|OMEd>e` zx8m;Z(4vF8Td^X=eekwOan}OH-Q6h^m%&|1k-;6NgT8_G|L%Y9Tld|6y{uWYl9Q8U zXG@Zk?CdRaKJwf4PPj4aP{n2UPQWtakRQ>`_lKO%>a5mX%9UcH7oO${;!9nK$5a}0 zK*`tSaXkz3aC-e$>C*T3)9d7;l{U@}NZXNt*J97fsyvhiXu`tc>AAjGV!D{7hizI> zMGI;Y6#X8{zneMSCSq`@XE&!S99iOGUm@@1d9orzpPrm2>%Ns<;3sUK7=Ea~#M#VO zefqPrb=fi|BqQ|f%=z&X0j6{d^ODUXO&6W_CBhrQg1!EyPs8k9FgJ&)G=CF(gQyfS z?NLogKNcKW zS@yU&Bo^r)1G((;c`aZ__!ViLh0`Y`e$^ zQ}fV8@BQXhoTM~+t7I9P{071Ppi8Uu^E+l?Zi6_cU}Kute&6~4fa5L(H+9s$+Tf=& z6e?x4Ek=3dX)ck~RIcfbFzzL-`FsNz55ia3d`DWCal*5VWR%$KF;t1-@TWWd^0IA=K8J@?9Jads)g6S@tlC`#2_*BcQF96X zBJFD5!>BPuYFbGiq%w?HlK55h>+&hilPc2QYcvi+;f_ z7hW$|k?NXE#t`O{=6-z^-(vQ;3zMqGQ@w`$0|^A~{8&J(r!leBQU8~Vm_GTM5)^0% zQ$4h>pU^uh#5mJOz#RAm@5T!uwf~D8@BNSd?kWKX5l>8PF2{Jea6*f9UXZ?se!{U~ zem1*Ds6OIPhr&iiM)rjK$z&rfj8w%|p`0knr5oPu+AcPPTI43#> z^Y8Z4H_hm#yY2{K#Xkx_)16`(Swv6F=G%upGgd&=4R=Y*Ap1l-Av2X#4l5&KRG((P z)AK-paH)m$+@I53pQ7RidFhM479-ilpdvWkf12HlAglcOwbWV1g*EK*l}nE#Z#aO3@3l^~N3)%9+AE4D)#059 zw9e@_E_GooGxmEjN@f!?D7@Eqi zmdg2KO$AobEO+5jFH$$+q@8E2#zll7Z>i#t(Hs0%cm`!-BN`sWshP}wr8?$9i@lU@ zkRkI6mYI&BZv8A0gUg%GmiVyMXu{@1(SatjpkUEi*wb>ON}EiJbPQ48)LTj-onyfB z0TY~v^`tn=+Y1JD%BD&uzq&K_C2XRZqu!4?=o!B+rnqrF1)z6_3RClQy|&R^K)g;R zA>&sQNu+q5TN%vBRb`|();~I{fq}W);+1avWyWdNhVH4F^wxmlSjrF^kV9}b%j%M` zh;fD+KQ$q}@ZLLo{HrJXd0L>uhk8ZTv&@%=$$6@FCsk4gXJos)N2X&!$!R&`T-Vg@ z2f?4h7`Fn6AtU-+)>1i+nNZ3KcV9ch9)%A2)n%^DVL8xA&Wz_*$h8IaCTVwzMUT0S zFN?zUP1LB*)>Ny7uR-55+@q^`+h07#8JfUbZM-i>(~mHY z2!&p-|wprMAALzbt~hoyZ>9kVes?vQxtm7eV8%l*YvTwOawim*=>hyv!s?Ud$S8oS93gpwIr1c?k1R{86R9PdsVj95; z!oww8V%^^95$Q9q&(j|ifYVJ9=_%R*zWq`L2E}xUZ_6v~7d?CoQWL zVZ^saLlFrJhledH1WDXu*j9T3stVYM%aYfN-kOgI8F^_G&m#^9Lmf@l;u2 zzd@8h2b~?4fYH1`HOf%gCq$ez#hl(bArREB_cjF-oLHk^S6(ULF8uKgY&&c~n5a-c z1Du!(Z(+xPskJn8ueE&ZSG%D$_BxHa;Mi95_2Hex{pCIrVAe(Q9thpp==Op@1$QGY zCQw!na98g+MT>Si^X`42HKbo#G&7I_5d-=)T0v(TqHfWgADSS6MwGQXw~8rEGq<56 zwJR4-ymkNe@EGvGA_{Dl$3#Zpp8s^#k?~ zeZ+Iuq0xrrZ$Fofj*V}%?bFW)>Td)OfAi1@jqmZleuV@!?MgBA7@7_~i>!ty1!aUD}eQCmm$lCggfq z&=BUaE-yg}Q+fJqm5Tqeg)fvvAU5b0WF#ogt47Nl-b*7t`J?cj^wOfOTLBw~ck?aT zQ}?9z9&IO2KpBD2qj)N|?%%Ah5Sh~ZZ$5hYb=DnXGXpJ6GID1Po$!3I>`QPD6_qw#b!=-RMB>bx65yU2GTJzeNohqP zcL;kiDCJCD+ibv9>Iq)-1Z9JYuWs5+#5i1~P@NUH_mUwi zlVep4fIx8m@7*bs%kkrO(T_!RmUJ{yNzA#W?z{G9%IV8>1-Q;S>c$^M5Meo$&_j7r}&K506+(+W{F9(~{7q3~DFHlvf^&{oMM8Zq!%k1tL!1sdyT@&*A#6Lx!1^8 zO|p2Dlb`uXe=VKy_Hn8$e|;nUMp|WT#|8JUR&X%EZ{1z?ay6WtMV20XQR9S+1V{h% zb9(BQ8^XfyrEbBlr?06R$ezrsVqii-+Q8YD2KEhmRk#h~62)#botH8E28MU3@nx-> z^W^e*0lMPf8&{o7dun*;>JKJjf_@G0j`DohM-(s7pAr>yf(Yx8wd7TwDQv^sH&S== z>Z|Rgf+;0W+4_a$}jevDz5)pB)XiKTsMB-Oxs)7;=z|&15zI?)gM0p zt-W`nnS9z0@v*Ns{JEsgjVFER=p?d?t2-JJ1&CTr+n6mb7k&HgJ&R~_#u>v%xXH<- zOv<@QJ8*7qH(#1=fB15Xv~KB|TK6<%uvZZHodogcWwRo5iP6sfayq|=z%%1hY~{Y~G*yd?=NbI< zT4XPygFoyrZ`${xS`I?1r>N)@aNn2LL&~RCh;k&KHE;E7ZwG;w4Np^byhN^iyqE~>YX@Q>igh1 z-~|@Qq+qn(CViFp87Vex4i+&Ik}d6IbUKLZGbb}K3Gv!bIy69|j_-29rx}rrlyZ7< zr8|X-V;=SRgK`oH}q?% z2_LZu7XxUC5s9mAZ3R*SQ+cby!W5nlUTvMh$1tg9 zS!aw3WtC2wc7y;TVH#n0o#=_tmjDT}7&qV`7q;u+E!2x3ngUP&9y_t><9sV4-Qr+z z>+3(aKk`fXLF;jR)YI*5KAyrdA=1Ic#lM!REIo7d-o_jZxpkTI5f3+B-D&Lz?!{?4 zP^$jYl%eCqXLURCS%3|0dwc4uh(bFENEIjTu%=Q@S#OMaNLT%3e&xfHF2J5Fgb-i5 zfIu;mFO#l8c-Ba32|R0``-KizP9Xhob#9zHgghRkMWQ20BArJmf|-sjzQ8gzQ-Y80 zzfh#h`c$QVlogjrGB!C%+uqQvo#fE*R#qoSq!V_hs;{r_O!WPGOUp~m1_zN6?6Zvh ziw`E0I%0a$OCKJTE8>p2l2XVIA@sZ#upv1k>pz76DT zyrYi(L7Yj7{RJP1FgBi5Lqje)7W*p}@s0(>$bxdnDi;xeE!>v#16;x@JF1bbIF0By z%>kRvvdl{Hpkuv;ca~n10lnT^S%(&+NT~sU9Yuet=gJEjwL~PfI5?jy&IXIDaM@iT z7S^y+;_(oWUoQY*jMTCpv_z2Axt~r!7CQnzGEljEwSv3r73cm-5Lu>|B1EF=(8n16 zkW0O-L59luY~FTiZAA(JAG#yQ;m3-A@wWdEZp0LB! zIP}XqgR<=))~{k!h8pj3#d&?>VVkJi>*PI`DxsuG`GX(YdweN_vX!J!?TK-QdvFt3 zHzkPe9L&UAQOwx;?+`hjekB`Q`OsEoPLAqT)f=s(yX0}8(0VYn^15c(vCy_#xXZlUE_?L z#TDr%hOO&s0BpYp5v6SSQtJpQECwrDY*pVs+Uw-HJ3xLtTx^nP5`a!tRP5=8S@KviD#X*u@XJxIvf zqCA$nmEH7F%0R|r!J;51emYuisS=`x_U#1`TDkluhX^q?|8oF#-=e~|hOqiX$+sM1 zB6XRoJ(LP|w?{OvtyLchiQFNu$Mr96|AGFi;YO721^(~)R01SPey}Y&n#i(Mew)x0 zBf~=6RXCF4WfffgC`O0W^0p-(H9GOVY`*H#z+WGb@p00-Yj!ve+cH3}nln^U*N1RV zQxu53+-6H&n=Ya?vXz=|O;$5izJpHb!h@y^MvNa1=5s#k( z_X2m;)g2`(rGd);*}HwBO$}}2`PLXi0r{2pVspa#kJH_nl2_0nfcdv+)7i`rnPz|4 z1AIMKxUqNm{tn$|E!@2GCv~TL@A_l7j5X#H7a3wXzV4`gM7C1eEQQ@2IJBL3yo;r; zdQ$4<=GdJq)xRR2BfEOgrO_FpSRH&=AUl(KLx~+eR)#+1w3=> zD%>RdVkbHT%7gIz;3rG@TpFSnS*%pIy!fm-9SLH(8;)0>UDxhn8ePA*hu{LTGfw;@ z`7mu)9Fjh_gu4Z!66kW>iKs^cHR5AJkO%3S`3pNQprHX|rp?e7cJcO5AoE%8`m=ZW z0ZLE9YnojTK+1dAF)u507I=lOXK=YGtu2{|E}h~!2xgaf;``wi<7!wNkY%X2_>!ut zJ#xP|ySWQx|%JNn3ohParE`CGzR6AZO=zP8)p^PWIrad(^U0+H#-Dl(G` z`8LiY0dufk6uEH1&c$0IpsJznOIx}FM!|2(ZcIP-zUV8#VLZXaCZa)UM$*{%Hfgo) zax^C#cg16ZNFXc1VQ^>E3pM`fmVZx8(F_kGDJyxr6r%ZwjbH3xn#%KKiSR0DE$o67!Q^BkcafEF(UU2-mYc z5x~3n?9N4+F(KO=61J~GBeeMiaw)_7qV$Iuw&@g^OolOT2z6b0V84Om;`dfY9W{D#4rSTt3G_r8_`( zf&10T2y5!aopz%8=&TCV5WRe#CNWi(!!Ky`K@>7h{yzLLgP>J}8}W zlFNOj__5RNWueyM_OoIfCIjJ#9|Q{|XDt;l#t2D#k*DN%TpuY6H_}}OKwT^QbooPN zWRmAReD%DFkZ{rM95aaO*;smGC5i^f)YrHT-Ms#TUx7Jz`nl8GPUEK)o?=fNU=%+H|8>L#nU__TK zo{4`c!PRPsl&#lpsio(;V=T`7*xI7ufgA)HB81G-jxF~3UW5jr7`%awu@r@1eC7td z^ru=hXxXy_u}l%WAmGeMx1_Ts3MqxEV1q|bcKb- zXwehs-~YkfmnrKus3w{NmY$N0pJ6_XSz5?wQj|U7~RgzUH#ZG^mF9oTrbrT z4|MwlLdYIZN{!;a*K(999S?BFYZwOb5U&-Wn zmoi&%ay4RuO<3cFP1c7s%8m$=v&z{uN#_^NX>enL1#^`Xl@7Kl&Z>mwt?64_`#c+13 z4K4;nE^Kz4y7C{^1sO_wSe13TWj1W9y7TQj)8*9kjG`6#j! zPu!}i8p3wlyKH1D15T^gd2A>?FIeaoPTY#pA90&67mAyT=5rN}uRAUbwjg+!5r`@(o5^%qq zulON+y~xhGAJLm+L0|V%d%c|CqSe=B#r=|Zw_!LQ+!lA(?^EH$QP=TPv1dO@sELg* zb@^;!s{NDnGEw?!qtOX5r+Ga~A8ZS3RGkT>APc){4$?E4v#IMP{-PJWZd#YWS^p1(D;ftpuGnUIY z#3|BmI?m2!1spQF(;;y;&PRSnHHW8OBKh~ePe&#KebRfkBoN{>+ z)1tJr7I~X?M?#<%?Jt+W%V2#eJZI3+8=LJKyz5rEl3N3hr@p6gnSYMku9uHQTwrJS zA*6M$Z+;?gwYVP*ANd@4&91mP4p-_nRVTbqOb~C~Oqj1uXh0R(1r9~9tqS&(->EMp zG8ph}=B!CtU1?`jPr~M=&6A0H(8_I3tt%LM@5gTOUFut7cc?$FygnwT@f-4Pmer53 z_5w6(25Omg|MUC%;G9U#P{{WicBVEGxH;5?g@uoJEc(yT(b3zdZex|Tkz-So z+1!eym$w`3q+O||XJ@7Flyv$&#`w&XS4K7~(`KCHlLL*$2O-_B zp=(ZN$k5GXTPnA=3qOKGRp z$~~@*UOg3jJ=2dbn4?=89m2!dr5D zR1vSr4X%~Go$=CXZ!8y{uX}Vi&AMxBv2tLKmp16ym5BUGV1v3g-|Yt|1~WUX zOKE=(G|emmEoKjf6tSMVX7}X{y*|+O*m-Bkw9a1Vx{{G5VwSW&6WF#nn?6y4oYbBF z4v5eGX3L6bI$U$d*eA0QvtO9v6 zS2fLQn9O@$XMvb`v2zx+yUtRxcWfo>;OSaS-isRNOxq%|a&rJh+hx~>1iz!LxXT%G zLR1PIvA3Vm)AAjoWd}bKEFF58u`~Y(L}_?2Qa0z9GIUL!h6&6WmUk!~>zQ<0u0KDk?Vy+Y{eh|jV1akq zTsofR?05qr)kohG^(-vWst8&hd99|mS`%{QFjKjsrdJMOOuQ}wZ~0!yxGsqy$iQ2D zH)c!TC#!6{hgq1K250(}pP#$5K~}s5&}ms3YK(42RR)-#-WHol@l3usWCOwrtOb+I znZwn(XBx5J5S$W~a&B)4FH+1Cl|;_=fpXB*Vy{}G+grsZAB&Wc*kR521Go8CKbn56 zJ4ac&Ofe5I*|ZBw!51bt49$x&IQzDH=j3pJlc?qn2SKw#8;AS_T5cd*L=m6s6h-E$ ztpM-CCY#;K54>#}m$9+b)clFqvz8rSb7H6W;}+j1w&hBM*)AOHCrt_`vGg}c*J5Ta z3a|Q>ViKJU99#k{YD%}ZBd(Wnc8^{s@UJpuE*5xSoAiM-<+>Sxkf7Y|q)l5N#w*?6 zbk0os`a>ySK%Q*2S7^@6NlIBPh{W~7zy_(S^^~n<`*eXyrVjOGkEnE_yGUHWa4~6t zb3OWJZlg=AYl^_lS!5%>Fs8g3#8Os>=Y{4+D%EPVmoN7g?K`GLZzn}}R__j0V@Y7P z7wPxDe?q>xUcE1esfs^ZDU2gg{2TWHHfjvsN!>Mb2JT)mLw z$=dPp=!pnqAL3mz-O95ntac0xDkFRE*CDlh2%UH~bAzSPyK}V7zxr+Y^-8phyw7)B zNquvpC_?5ISB;6aq2AzpxDRfy8$7dqH+_Dvd-ZQbujfIA3%&rlQiMI~EV$C8u88XM z7~f|ZYT=HE+7#{}&$jAoae!(XBR+R1yiDTER_%P(`AMt+ebDD8aBy-pELbS?bV6AU zO{|65K0R85lZI?r{_)^Fl~3jkb}|CDUeGOd^TF_Swp3^>Pd zn*BwWZh#9~*UcZ+*q@ML6WRUnjCcCo_C{A*^CGj2L3-)Tc*yLsT@4`-G{3e=$2Gf7 zVb)0a+qiFmdQwHkB`M&ddCC3lu*nOuglJ{BXfA5|y1_+0txi5Ec9T|ouWow8PX)0g zNMr-$oWgI?0dDv(V~Ryo9f}2`J;1qas?oiF~~*VPWL6UfcZP7WA^T@Z7k>YRv%Et2GE z;_i;_Z#xFi5js&|j{Rg~>+M%FHe}#RCEVQHsvmCL=D|#$h{pApJk(doLL%TI6YFxY zP)8;G@nW|?^ma#X`StTa*Ppba&@ZyGvM_M`agMKVoBi(FsjODpeFppcWt=2neeT?a zHM}{X3#T3MU}9A%QzU`PQ;75yW^zx-NkYs7+Z@TC`+R$x5_;E_oSapjNg z2z#C^ick~|6P8j;Q!Mg${Xb|-TZF9oRRjbCKv$8Gkj`%*tF-JC##S)AcM?9Q#t01y z)6mx+ru&0xGOKqIT}pmelTiL+f;|5V|A*QFjyK+DXe1OD7gMpz!Z;OXigSd;+ocO; za<3R>U`QP+2fI=P#t;777$O4Xw6xOv{QTjne_F@Pd9~sRCTTkx6%1GHY<>f?oT-`O z=<@bi4s0O>3v)~1Ij`R0!tE^`{tr4^cWs7ad|LGHkJCoIpkRn0n0PNs{=}n!5A@Ab{-g z=j4AOHFgGY36OtQL6LEND2Bu2`forICkp(@GygyGej+lv|694aM8QAhG+^cazw=66 z{;gcH=HK=tAItskdB%Z;|6We}Zz|gQfxIYf+y92ms@6X#vgrp>;b@orTi;FproMGw?tnzW8meD*FLHh{vy-s0bs z>LdAr>i&%mGX;NBxgTqd+3@AvFyJZ;UEJMET3Ynx=jRuioWZI4>jQDLsi~=|8XD87 zRLqZccJ3}#nFb@l*f=ipM&iMABqIZSa*%ZP)S1t;d@JtUg22;%H(Jah> zF*u3GiHwp`+`i+so`ZwKaPaO=RfFF5w-DCbtj@zjy9WvLE4qF<5k~?CSBe)G7gtJTl?~d;C~n=xny*PD4^sa>$UCOL9VX<`y>p%D#nXC87Mo%CLdp`NYHo6~JZO zr0p9W=VK|@pG*jS)ZaMSa~A!^=zR=-^9MZJl)}0jYY23Fu+a+#7rj7?E)5$`5(5cQ z;3|7r3|r{{0=_pMv6PQ82Uql&5JlU*5LWPSz@qpM;@NbZds1Wn*ad(qt%`}k1jLS5 z$T?9x)Fq(VD9#B_;2y}jQxK-H|A`1SbO^j{DT-* z+02#$V$n$i;$h?DLl9$d(U@>Y)nhN z=$7J5!E6Wr4bLC!2>)Mkm3RWUnYsS{5UK%-8ft#5$M>UU=?23u9+wV}X)5^Vh^4&p zo_~l5d-T6zPxfIdt&A~faZ`wO*&fUK2f~rS7kIHGPb!%n1_87t{c^M5KcK0`cdyB~ zJ?pH_MWiQ3hqKxG)e{z*yWST?`!8Uu|BEq#{`41CQYHk3tH0sD#~&CVNJ>hI3c!=b zJP`Bvj=#D%8mtGfw*LgO=Q4E0ZSP%lf}@ zCd}G8wcsCAMQ8tIOKN6(RHQhPr*I$CVRw?^sYpb9h5mspE-{e?kR67*hsgae0O7>m z{sZj)h{GuC&&|cbK>?_q{{V>m(@-vZz8D%ea+_YZ{5Yh8gbf-${#m`uc3k{IX7A1m|6-gMx5B zs-52_>;J%(T3gEnz%r=SX8lhDFdIhX(yad}Xw%B#unztI5VU_6z(=7orfhLLNTu2t z%M$-D2w&i_PzgRY)_I)SotpGv;`Z3VT)N=yn1?Y3@B-=6cQa&oIHKvEFg$8NU({=* ze+>fDxp{ej*zN2d;G6$s1qLIsF<3`!ht5N%9j%K2TOp zmknl7Q}7wwT;czF@T6g8R*03rW1e|o{l9Qg@+n=9f|Al6s~=RZx*M+Ytzm^$tXY+I zEb(*ygt=<-F!a|VU-#J_dhkJ6-t+TCQ1&oA3&yj4@ENrrDjXwrGy6{;)KaCTDTIf2 zD*Zoq37no~-U~f2S|xpWWTds38P)u|hfKI=RT;X{SKp15?@w9pV;^dr!zLB-fU*2z zHbN-JVg!_vdS1OIdA>|9}?%3BCT4 zcmB}y4_s;E|22wMvM4BdVLy|hi34z?#HT2#Nns6paPf#_0YJ4AlatgbW>vZgHY01@ zFjY2KpxXE=sOO&VmXlLliqGsCh`V2*;NM#gD%%AZLfUVo#8;YL z`V-JMm_+Rk*7x4EoP?&MUQo+cdCXe3D%eD)_V*`UTUcXlgssr3KDH6 zubw?Mqc7C-v0Y33{J50v3={CWET!6c>GxY4CLoEhnP?_}K-j9ISTOR-@35ygsrYV% zrgK@5`xGQ1S3&g?&FjxBC%Ch~@6u}y1HM##etW)3y)!!Ewcwe{vxQqg+jnu(q)J)LuoPdgEMr4k2F!HNk79Gb7}mX1=$p@e7l;~nl91Gm-8MTNZn|%{ce;@M zgmAmDSWihUM-ArL96-Qm#7!N}-#k6LShBKWcyW2L+hV^US@MSy9)`l)+P+m^>b^6a zQhbMe5q1sH(yaunNwV1+!eHj2Y z6?VE#pdMIfA28G**PE%$SlOEr2S?{C=)|s4nroX^6so)GtWnr=L}gsM?5vps7Q6@ zoD-+H9rzer4(B0pGlO$jE`;2yO$J#9sNx2w+DhP4-5_ecCY~0C!4k8mas1^` zX`m3+4Aw9G^(xvZF1D&leF-o2`U2uKYsP&-FQAgaGJ?~!LiK`;t7V0-Q%(lv^4@+T ztg5@trP{aos5)mK7+6XsM!O`7-w!ul6rR}w_g$<8Uq_q8(o>4_H|c5UYUXiVPsD1_ z?71Zz=mXY{2qJ_I)G|%Q?lZkOve9&%Z5bF!WVGj&IZq|hm)i(alGVj%0PrasPVw&< zRdqC)P@KE4Ch)N0zUYCaQk3N z%)TJ>l6acPJccBUGD>h-u(h(>)LXwP4#CM&ykhwF=S6WtcizgV(1E>m{PI|oknluK zMf*;u_b(qi>qLJ@QxI~7jmI}F0?$o>1?`qGw1d|wTK4uoCXWzjP!1}kGU5vkc&EAX zR|0l4B?EmDLaCGrZzI(@e7iF|#vS66&Zm>yv!!EYR68q*o+kkX4R#NfvW2Ci3Wt-a zVm1hx3s@I{+bpuz}OIIR4XuKB6 zmR)=sb#PwmSQ~DX(cjC>T2Lt8l+L0MWG(RY=2Tu@HgvzzXW=fPJxnd``%!wV%{Q&` z07F~)g&d2)cQZW0 zu(wuWI!wXHtZX78)XxHE+a0((+g0)rhEEP)hsOAO7%KPHJ zB&G4*s{6GQ)23i-HVL%1meK~9piPRqUr2gzkO$GPtNldcG=;d(8>&(|$?aZkRg(si zVA-iXu(BRMgb`Zw%_%VF!7(Omk4{Rg0Zuj6PoVkSUh0j?N7{W( zjrs;fbH_UYw+d!!*<6e``bz}RC-?Sga5(OneN^-V+b*F)wL^Hg~Ex$tFY| zocV^5Zwh02^!cLEL3ccev&McRNL6E4cR^mXyWZ1?l)K_kXcG;ul!YM zw@fq{vrrZb{<6?)f>#?kAX+*@q%@`9!Fflq?$hiA!fAl>AS1(x*R%mIwTi_>9a;0u zRMW)C2Z)7_gqK`@k+vU2Bf)RFE=zR%U5>s?v{-+RQ!9|EA_px*c`9FANT+NsZRXU! zgRfa;`)(t~%b1KIKucYx_OtLKhW#T#KR;2O_{?|(-4JRyK&9$nck_+Ha6TUga@ z;%0puBW-<~KRI@Cs}E@oIxku1L20p58Trnu@os4Re2&t`b-EF)BPd9SVKIf;N>#j9 zI`^#Dwbe1pbj1_G+B}bUCsU<`AWnK9WASFSl;U7@eSA~!9ZB>n1MAle8U`QH4^ygG zE(kND6KmVH{8tK-%h=gZw}aYuh{N@bihE#5mA?(6|`ik0Iig{|+) z-N$o#HsV?cxh^9uur2Rn5URnsHo*O=c4$7O)A>g~WnoIr1sgf7{5GkcM0oyRt_;Yn zsraO0KaRyL*9R93ON$qFEBP;|t)vV{+Q#?Z+s_8{@~S5%uy{(jKC~{1LBLdV6i7Zh2Ayp5b;Sc#`2Dz-7>)G1_9FLNZB! zxGCWDO_P6TVs72dXA+Yi_+4}^Co3iWB+k@pz2n&%#0 z5I2#d3Qfujt+!@SxZ6o@)la+5Ks0$o?re854Tr)RtkKr1dKD7~i?uLM=(dZR-#*&* z6w}F@aJ*4921@Y@LTp;zUE%hXUH0#MHF3Da7qrT2-OVYk(5BP2Eh(HGPyJZ9i3I#w zv*;m&ebKOV`pJ5!MU7uar~069da=)!e2Bs^oIfHRgxj{_B6Ha^dN*8VE-<0~%Jnvw zIE&!8M%~?|!wGL*nyRB0*=yPj8fD!E*7xvSC<-X_U4KI2)8fmMLI%*`neOmWM^uRz z&kE=vTraLHy!y$0d(}@)BH$$9YOZMo6cbiq2k-oh5y06plilGjE)~tDFl|+{gKow+ z&qW6g(5X&l&BOeJ*)o@eIo@X4bS4zKGTXh=%(ZG-iXosMsuwGCPWz2(gQZ z#_N?)x|$CT$ZG*Lvv#BID69pl!H0o6A@aTSsq)URt(?Ke%%VB*RSL*aZ55Qk=sW!t z=p!>hG7Tk6AU@HF=&xr)gya6?(3Gk+{c-cON7}Le>I!`ExCIDvK@*ONA_EBMl%KL; zRYf!w>J15=*qzu$EZbyQHu1<7VJpwFvU*>N?95qTGNCNYL;1eyOUjefFTjbFl7JO- zv~xF^d{o;3M&+zF?YbU8Gli7k0EAeMi(aChE7LU7HS_y#b|NY2=1Y79n@&WpcR(7h zdEC|j8_5JG!44IVjygYYZBm>;emn1WVJ+^uAWX&-L3OQ#vU+@_SUFNOY1?kb_Yh&p z%eZUv7>djVQljzN{lRz(8%ko)`~uTsR7a1j^DYP;5D(Ztmc;=bx9*Z zH}M@xO1p|vV&Dm!WqNRin0~+xjA6Ts{c8?XhR0sCyG^Q@+ZPO~6RBfeWpJ`FHW;uL!A=xC4 z#*abS$H~gDf4=5DHNX$%j|#R=RLOOYPDYLRWLzy>zEOW7`<30W<+fuc%@b!=TLgHW z)6lxf#Jp{A$!!hEY}M&2l~0r$@lB=QF~i(77gqVQoj_W9i$Aj6+a}=0bo@SnncWisE;F0?kADN4QRtI;_qnab))!wj9f&~Wb zzDde9Ut-*xwv}br0Xb!bzN?ozlc(O}N^Wrw#Y_6NFPTNa240P3rb<|ycbe=Aj;S>i zIjB6Ul?11g03{>08D4@_Rh8BwR*ap=U`O-ub<;EhV{*bL+M_A5^XqlGQR2LU0+fQ} z3Z@L+9U@ZT0?DQ@g~r$4SwY2aq!vY&V-*_QTk7sdlxTbgx;Ftkj&<&+I|t2kAQ2^n zMx5!r#>AujQjTdH$acPI6;?kvd%nkDW$!_9_%7`Q&}8H@<^Au<{5tWW6ikNZmu_M8 zo7B4$Xmrs!g{ULj;|)edUR@QaaR8LxIQ{f9=7oK~VqXgv}v~<03n0253(8TdQ_TLdeCF!CGb~ z!^G(v(fl-CQ|eb7@hxS_i1oG%wTjzYBP&mLIuT3Kd-X~{h2bAAUj)8x_K}95P&}%(JX<|=4)_QlC6cRA~@NCDzBiRq_R%FS( zqP3Il$g83la)lB$3*;*^?X_8*NkUS9(AAnG>=t7D-ftS~$O}vU>dh3cZIl)c=Rl*O z@V2iR@MuIe$n!IEfr)LDP9t;-O&j~1xQ6KL^nKa=o>vNb7h7qr&!dcIa;5Y^n=e1x zcJ0O6N#(8b#<)O!ZYnr#>ZMs>9nK-f@S-fsM z$T%yw_B<=4U@+PwgH=x@g9~5B#@M(&Cta|cs7i14ZZ!$TewSd;Q0pu$227X1eH|_v zO+LWHq>(|4iI_z~Vi5Df{DW3_HSHf>eRTzZor#z%7Vn-e6>8mflUO&oDa9CUPXfL7 zSerTVmvOYYv?RC$dxNqETB$<<(or6AIP$Mv*%g< z!b17(*5uB7k@KMSg~j?J> zi!iFUVzk~X^y`$hE@YuO`%0w7Hc(b8u^tcKORm5LvbEmNjJh20wVC>D{%N>yxnZ!K z5c}}BsGAZq9_y~jT!-kg2L-+KB|iI7Gf-h}pU0=v%XL@ckN3ep`>vA?@jpG)Yp*$B4Y^!mP;JE^wLHcz0{#kV~N_@_(-@=s0@uvEcN@C zD){{KkmOhhL7m3tBHnA1s=P*%z-gODgm+4HKSRtdEvw@mJc#f`SvlIB-VF0uh)pUW zRufwMfx|gQ{EfjfLq&UTkw@}>Op}0el;E)LG=soRw%UH?;=MUt#+yFZIIuRzTXWQNasAE*8zpg-zevu9?9)1z*(zB z`x6mm;rV7OYBh{C7#0~CL0N*E}si)XLJOQ{h`E>W<+N*kJnf0>9)(tzWJxhL<# zp#q3`X>$3TD6OXy=ru>j@JZWv>19Y^3c{7>Z@$L6)zy#5sztk4C)%M|JnZr&SSovm z?7f;tVT}>|M9?>rlFaA4;Qh7O%@1vL@9H@~(;)&=X#LIs+@S*BPo?(~HLD(&c;|kB zwx>G{93arQNI|4miARs91jGrZy`NxAA#f+@p{CCA>+LQs3-Wa>kW&WY=6-^UOyc+t z{k_U4p^F1&r1d_vs;VjYQRM$cr^11bddJzJehDGR>m{y%PHs@PI{DAXT@9dvpmAOEh^sJ^f2qJj+$C`oA{$B&8@yYLZKRB3P3{%8GADy{qNPNQ{X zxv*0IUp-K%T9oRGuI@@L>fC#~KS*rPhCNt(6FT!mTy2*SM4D zY~U_VQP(PTWSs2~gpPn$UD2u#9X_Fpje(s|RE-jEYv`(x&&TO&8rOs5aKGiY_wgUb z;K!Di$ia^=&igkn6VKQ@fF|D0oASYW70tPnilDZjb-TRWQ^r0nsn+92N5 z=<;$`i{6TECxi8Rwn0iap+&$!AiNt>RL8=?GBC^lc@^pVN>Xy3(la>p@fCW@juagx zr2i`3W_*75`}e4}r;N)JW)XZU-hqMRh`8`y9?!=m;(vG2hw<#l_IwWpzb*SUpn)i*1^!CPCp`q7D>W}-p+IXMM4glfvy1LYi zowC*fhZlQK1$YYcBV;IPcDpUOdre9tkVeC=t~}#`%a!PyLs!NnGsiMZCZrW59fIpJ z)M%z`_9Z^dx!G7I;u>n~1@M!fNNGgA&zzokT3T6QN6XARZv$R=lBUa5Zp+GY1F=t4 zevNkM@2gV*JVTRysz)_$ptnQ*)+OH*_X3A4a@8{~kA!5_vi3hv3f+|6gW1Obi#gGg zG>)DH%z=tf0^naiPOvqYg0HRn0$Qr=EPV!dC)Xt9O4`UML3V8eJ{);&QF6L(j zaOQQ|lQ~*?K>Xyc+@_LsX`j-RPKeKXwr?oV|{geK06cUinpLTkLKGk$~b_Px+d987<3_h`ToTz38NCw>tzX>0Q=NcLslvGrT zXT-0A{2A&E5H)Z4&QJ~D`GYJg#x#wMA%8Fj@qnVaxjBpAbFXjx#=A;Q7Z(;foNAu_ zE>u=l7JzfS4nO0%{-poLoqhWkDT%t++S+=Cm688TGeliHtGn1gnAY#h|GxGgWZmzq zsEgKU2nHCt$5LM|wfGFc%ooMNBM=M~T2>hbVij8Z+HU0^{?tq9RcR&%i)_4|lY_R~ z7IRhP!Ckhsp}Hk2J^=xoA95@Aqc!h%fO2?vuMlYQBqqVL8lq-t`9axmRd2rIuT%pa z<#bgyw~FHJT88~4uyY+DqCy^}ugu57-uvbK`}1V=X8o*gr%DR;-;-H)$*g;CF=!hQ zP_ELwy}d$wBamgVbC&<_SNAtxlhfVYx|owKY%4}@Ba&WK}YYIl@q`bc;A9vnh=Ck8eQCDf~YOLmf z*WP0AZ={)o4PaBh;nnUl&Q+a;_Bk2p_DmJG zU7$su>VpwV==X3trviz*IL4W$b=q~Vi|s7833cNrJe8!Ke)Wrx+iE? z$UuAgWnyc8Ay#kWa>|~cSf@k8)JFZcdG9-Icb?7Re*|<>h^8HSk91BT0-zjy(rX8^ zU5@Es%M2QMpS@IVL6v$&q$$^o4a(<0hOXExM6@q(XMf>_FsImVL0aD!sCU?J48QM! z{(u&R2IOmq&8+QLEhQF|(~A$sq=+87aQ1x}>0`ngj+^8E&<|ew53}Afxa=CLnd_%q zmshAlT4n5D)`nr1N|@N;YC1kViNZHzwrxSLioa~v~Gi-d1b>ghb zI?UuW6u{g4dj|#p+GQc9pkeRruZLLI?ssVCv>t0bWGIrTbQQ=$Z2+ou&-v( z4DgpDzCJ?(hRofpm$n>pwlVL=P!x~8I0F~+b#2PO^B@!_i#sC~Zp3MO%SY$KO~P%- z_1MVy9!^+#-Av$3uWf8@{;QDJ>^I~_l1h0kP^lR))Shkr##lU8$3uYGgLw*OH*S=v z5!5$u!s=X@XKR}B2oD`8E=A4kZ5uU=kA7L=k|8-h5VPvI6F!H`~PrrL4O@&G7-bTtzls@8xN> zxMchK_u&2sWqkAw0FQl^ngGDYG`4nV8`@V@xpK6>Rd$*#TmsQ$ji|<9O7{uk$f>8! z&k9~YD`+fHL8DGK-dRm$5I8|tKaB#(ozn3G2=l0$k?7aV`r<1>6xfW0e#8n>AVNcQ z6EVI7E;%jI0ARn}R915i@aCqY=+-iZ$Y-a2G9=@xct)fl-E-`oTvQ-HY6_Z8YuUi9*uplKFD;q?nGI<#z1APMcLAgcLP> z&&Lk=N(WN6;3}%2V7{QSDmGJ~Z%|S+{J6JJ$L&Lzdx;*yEnDQIqx77^1 zxJ_RgnQtATb$rs)0P$V>_*w9>Tc`aE#~N{7%aLZmgWmNT_uK}P8~MFZQXeasR)>KQS0Y+_?>(Wl?f0Vj=RTp z%B)671mhUL8!F?tIi#I~L-@MIj!}9E{AcMD^GxAb{yLyDlG8;I0|Ui}cs!Qif^BQj zbsH@S5?X9uIp$yHbQgWPMkSEA@=b0PJ6SJ;vLDZ%k&E$I%^svJ$SPI_mBh2v#U3xx zfQv}9cfGVFPYVn}lW*&$RV*J&svw6RI*-%)hTK-DaO9G<%G;=Vc#Wh&5PI^iE=T!k zZry5pG1PX(5H}6d%@_Al0D(Y0_)}}biP<<}@F)_DsJ^-Lx|Po3vF-*Z%~y#Rnd*oT)Or5#K zYR91`cae?Le$22i+gjavDKA}>bbb2!f=QyL8Na=^x15JguyQ5Rc$6J3Q~oUX5rm@f z8$EMju0^ttGHtwDFb`$4Og2K8AO1!p+5d`0G8AV3s%?;F5caht@f}5-9-J|N#6HZDTd0V^7eaE)bNqyoi2x`noYC=X6iT@ zwsCXhJzts-*%w()=i=D!NZlR(`d;D{L_8or`KjRbYxXf=o8E-Z<#g4FRUSJieeQXZ zQD5E7tCzgDMs$cR+m!)(bHSg%d3>S`2WZ-eLd^klbiQrp_VkP~CL>_)Ee@?7#ib?2hv`hb&WlbO12dLUy>kX9c zL9qBLHt`fXXshtvKf4CjI9iYah*ajDVoYbPt#c>$50o#}Y6;Njz?&lf`Z^BCuA zkZxsc_S;DN{jh7c5F3Zt6dJPp#`7dE)YMd8IH}}TX|5m3oh=)%b8_n2Ylr7Y(&9Ge z+kFrz{AG$wUj3GI5-94 zwod2oa-0BZ{Xls%QdPOxjra;8gpuZyvH{3TVfKpP{FcFbe1 zm)up9KqbS0ot`al9Wo;!@p3$?fn29_18~Dpy%UA+6wBsv$@cJsGY(%fGn^kA$snp_ zA!=Q35Id-4IjQ#f{dvT?(q^+08)I zxuXp1z1MJ%p4p=V?1SU3?W5f-OpooC2RS<>3^*hIN6*EOAhp9pdFto(EqZ%=NikcenSeMb;h1S_Z<<)9N zu!;vxV4`))+9v}Ozk|uLGX=#dZ|3nzaC*uce~ISu&50c?+F?#kqXFLs94pPf4?wE} zoESY5Cz0oCu^Gm_ByUB7E}fQ!s8b(qE7;DO%MEY_3}=5`?H2N|S-ka=R@9{M1aAUg z_tngxGO#_UN8yxKcl_mQK>G(dVt8BaLhb336KILjBiK3r>`qz~AV{ewwZ@&@@ps}! zdMD$4O+7JFq#kT8 zvo22b%Z)h3T?(2&`6!t1ol4Py>d3rB3cG0=a?!VB#7~cpyy|3#FpZ0iX%V<=$qCw( z0m%n^qlfPzN`^%Ph&yO^+{kGzT1+TU@{l?Pf^9cQBo1>1RJnXlHNg&=!vfzOx4RQ7 z-2+|>;7NxtI#7d*bOXn4)cv#vi0zH>q{h=^M0LNa`Dqr1+%AKEHl)#fJ*)Sj!c zKfcL=T_bQ1e$Zxx(H!4QV}J%{&u<2QnTTEY{bXIxpfr}a<5T;S|9 z7TkJ^1#h6$g=$Lc4*rztY%;up+|Vm0Q8C&u#gLEkRLXoF?odQdQ$Ye<3gscK=+7%v zNxYqQ(RZ(Ps?BMFBqrQBc`5YV&73{FM$yN~4|B6#GbIqd0^5MaZsj&i&5ZS_xk zv^HjT`df3KeuNP3L|64Z&PQs8vnxS+2FAQk=5mha!mHxe`Y!WX_BBWJ+Pe;vgf~#x z**Qk}^L@?@lu7v+$p3}2MBB|Mmz*Zt&aXLNwwXQ{X_PNCNs;egPBQ>D$C24|%zCX{uXmcV9J|OA%6E z2S)L6eb1_T!URD)@2fGKzJUNPu7;wCOZ|hc-6iiPn0F6w;!+)=0X&;vAXr9T_tFI=@jJUo~SGY=YE zj6)dqV^7z`cR~W-0sau5^uc_F)3*abj-@JxI_8JoGbL3XJQrwD?Do@~XLGWg6~Pb! zFK?{>tuzZiwU-$>z~4M37?g#4j0@m8Eoel}Yl(DQ&+Qd8XX$wdcMW;v$l%wvn?HiL z5$j%(dJ%CyHfkXsKcwtL6s*mfri+f3x}k=|H|$o_DiHRYWby-2v_9^RHqv84f6>Rf z1bz?;zvNHJ3L74~G1awmqS7M{wfC%lbSw8B>uL4FUE7rum3x-^!i=wIe723Gsls4S zZ}#rrPua5V@pYIP1NM>W*pufjRA!A>087p$ePJu2@ZhH&NFXOdRxws`K1D&4e(l-)`)2Rt!hJP%xjXwOGlMXPn<_Uug%e%< z6?Dt$lIk0u;!^MS4;7Meg$JCb*MDVGad=WRmRxFO-DU~58%ox|2l)mxmR%vKhwfz` z@oA@2JN0SbRy-}kRmdHGKT4h!LLrJ2^||H>iX)7M>oW4`zmTR6nw)Iwiu)ewbh#e@ zwgM)<-5s`^kF#GS2v{6yoR(`s%`^O%r=~jw);W8xKAUN1*xw@7^a!}Eg`rmMRt3{f z{TNCQYp%rk_*#RKNv%5 zLNy3?b6yqL9Sk<>-lZ$#wOsS`Anyn;sy|W`M|tV_JA*SP^me^9PJM{w2Yq~xPy@$C z2x1NG_E=rbu(ekX*}dsg^+6N=9K(AofeUpdWejAQwHIBHFKSdwpd__y0?VK(BRXHUvxXpE>+m>rg-GK&3K)4?Ue_#=gPrbcx<4f|PPsRD||N%h6= zHqN)Pi{(F>nH(A;RVg}An&i-lm#T1M)(ZSqR2<-I6b@&%EB0UpJ&_GTO41r*M;Fd> zc?W=%E{REOh8qr+uhvHlJARf8ky4j`PA(~Fx9F(u$gP(%|$g1^f;z4`PLg@ ztcMD$od&yEM0#SYfeY~!{c52+HS6+o_w{l}I*)WI3P&gE>!B9ip;1_=8G(5(QG7f? zU_viT-%dYVOYjlfz{#A>$K&K+z22!6Qsn(8|G4w;x!}~1q3ZyGVs~aleRDj;0LU> z(usrz(jZP5#!(Ay=44*Rs`&~@%GG?d|G>L@j?xIgY?MvnB zmSstCvJXpWArQ@?W1EDUg&P`L{x2TSOtz&ib_z~W7%`Q~a+Q4b6%4(`7#n9g&1c-p8=XH{7i8$!ia z61ue*4jH>S*+1U~MiLCd@ipWDp=st*xPln-veqS(KB}`fkxL_QM7TV#KmpwB{g9+ zq1!6KYzyIg9OOrFZGLs`T9uLRs%YsmvErx6%g^Fr_`N$AVfc0BDa`Ds1F(6>`Rg1C z#oy{BI(sA^fp!!RyB-~P-`F(l+q}N;4-4>!X%D|EF$xuS_gYN}Qcq~V=1b_B@p-u} zGWI^OpNrx_5R1k{Ck>yVV~kk}!r#ceV)Y!SawN<5>X;WMKD=3VY;rrH2g;xt#Qj@} zc6*SK=XuODwf|mZ;1;+Q)>|A|wx%?SO^x)H z?DSvG*v+-5=Y6Q^wNqKuD>XAVpJtPG_PB zl$v+j_*vn`0tKHSp1Ye(XHbn=(57>AIpm3poE-CUEO!;P^NcP{haqz?JDBqwTvxxm zV7+7RGmfLnZ8tinx0ozepu3?O(om|e7)76&5@D>GYT5*`v9oMi$hh{f&v4pHU6dMB zo;bQtWIKW+py+UnE_=(|#w9gWL;D(dT<1v_f+O7##{F&fM)A&HOoeLb=mQ{1kdm2N z#q=Gc<0;Eq;-SUP#y-5cJ)T4*GDpPkJ5|ULAkI97xXzvrahJQ1t(WYR@q9>1 z^VtL62VL$uq%N9jB`C7X5DSc8mB4 zmd?q{8;QdyBAl#-49{C;Kjj2bAfh4Eu!N%0Zu8JzUNe%0J5LiH(E7r;>rWPH7|H(wu;qp{`Ltq6?h_)lU8G#F777tU$Iz-Cmwo!h*lhCWnHa3xC%AkHseQP{A;z|eZ$f{ z3hVsgiXR=l5Or{<ySSD)KT(PzB7f|F0*crJ>Y{19;R4h+0s z;@O-}2))pyIG2Lw;Hz2IpJs#>B0c%mZxm*x?c8<&xx>tS{$6w>=CoaI53k@FrZ)>Z z|GCu|!uR#Hhh_(~lKd;~nCa#r0j4gM;HIg0+z5Dd-@hZ z3!ahRlc-4Ex>qEmYUyRRXlIi!7h;q$Q&)Cnr?#Enxq!)Sd<7zxTE{!httXImo|j`iXu_=W~mY zf--OuHuiY5Ur_J7(5h zb2R*s+ov||Y0R`TR)8Dy)Sm0Bx;95*h5Qxh)OD4cU8!Sr-oc}f5L&_;;a1!xm1stV z>9HRj{55|gy?A}w-@dX@R)BK#U`OSt%HwcAgzp2kPloM+DMkaE@10DL*-?tza`KG# z%5^+pPt7lf)O{;XS>nL(j^GYYsZ^g4hw`e-c}lFcyw{gVDei_YUKO}=%~p@kw)4@T zwuWHB^}6dcL+2em*UpiYuwDNZ8sY5-n!NmwKVO5fH;+MfIqz6q>4T}ssr&a*_@rsP zydktywt^YgME^dZRWorX#>WEdYxGD#G*~Mg<)?DpqH)eyZN78MPDO8bTHHKZKvTBu z)H|*KC!1|`d6QF*&LEdXalo+aEyI%4eXVfr?q_Nl=u#1Ny@P^9g#4Y%Grb+WvVp3?{dY zs=xQNK4HhmfV44t7f~a)L-JOK{L(M4ZIk`evWMFAEAqB(G6m#LN|^0tl=Fv4{vf0aySIbb;cws*D9=7qKT%(}_#mGw_9bM*NzL-ml` z+IkBL@Fp9z7EhWEf3xhUiu(^SpGcq{Nut2OU4!`wk?P!NG<^Simj;%n}1`cC!rsJMXe*$$&icD@Vpg8fG;Q` zb-h3qS4-=dx$U6dXl8FKa<{d5zA-DIp72Ok?U4rKiw2Mk?r-+x@oP<^gzdO%Bu!xw zFV7~uNPZq%^9AAMPipW=SA>XjAwpFniMDTq_|#J4Jbyk4v^UT)u+$Ra&E8-GS{ckh z`(7J=Ri$R9Ktf|AVVbNJaB#coEv7z<<>YzXfX-#9SU`)oJjvX6p@`>5HM@ffa-Qxt zv@-U8kTBx>e4sGId7Z+FEQbeH)u287HMtUQO2}-yH3H48mJbc)94HgmGm*6nCjWa zJT?UmV<-yfV__Jo)r~Z;fM7>ZuBh`n?csM z+O6iXE9$M=ZwCg4U3x#yXcRLkYO&)0o|S5Kc$@Hf1{o8UlD=cN#RMIiD&wcie&uKs zp3)e-AKuExBNoF$9*@Dj>1|^@F2-KB<#5xMG2}6qtV3^1QrS7Ojl30@Uw`m{5|wD4 z%xn)&Vb!6j#RCD_XECo{&XGkcGV(otuKNqqxd##R*FTG*XHbc+sqW=S7n_2zUYPGH zYf`=(OBtCFEsVFc&sT;x48=d1e*dG;Mx@YbSKgy)=}l^z-@esl3!g8AtC#9hSQr** zKS;E9j7{gV5Z=1x#OWks0+w=`XC_sr$zrtQ1!#11#^0Xh)k>fq4OwmdRX$d0k*f@G z8pOOSd*9ZlX1Vk>aRvIY|x3#}<4X?4huK`sYGXuCYcl*)OZz@h?LIKru z@M~4@Vg<~+&cehGtahhTz__C2p&VaO@%_;?q{LISXYTgJv8B-qB*uU?Grog{h-&=( z7t^meg(N@ka*OU_#q(mq>3ZXP5nhdo0w~&nNwf8fuulElmi!1Th~DHN$i@aviuk zpX8EB%5xuZTSTbI$rY=?Gz@dekE08gtt`oV$5DUdl3{CZCI&hBb@MjF{m#g`UFJKrWqq?A2&=0TR z&yi6|Q_Cvy7d#ibxoHwMeTYEP*e`Jw5pth8(aP`mw_XPL{C zKDGE%Zr5k88sW3f8~q5VP~Ym*2YUy&5(WTJG8*sguCHdICn>2msBw~uiJ%tN1|ouq zkisxf@Ln+ZtmYvtEy1LGyu3LGgb>KXgIt)Z{(XYkolED>1vaaVQF}0?`0pRdJF9;XPv;bm*nYL{7Ex_mrLCv z04De>M%~Pn>?A=<_}f?MJ%Ie-9n6%cgb(COqf=~^0JKh&`6g4>UNsZR3`da ztu4>iUn>aDl+DhUlam1A?(PHOo0+eBRL`=2^!CXL4i`&L{A2w7n@>XGt@h73|BJ`T zVSff|BqVE6>FMdMx6h5<{cSh_52JdKCNh8YF^NHQjLfg6JIgvgKE@IVoE{Hfd`-&w zlS?A?ePraR92Spfccp(}`d|4Zn_n{jIfrD^tds8SO9}kN-7$y0s|Jy#Et#X@fI+y<3<;$okFkpImyqz!34uq@nxb|DQ zl%!`IEj&E{Hj-99>ugEhj$Btt{-xA`FE5P$3?hCVv;7;fPU`#Em>xiZvS(!i|M``q zW;eV-0o0MPe6WTPtaUY0OrT{o!f0u(KG!umaqCOwD-oC)}kmsKW za+y~T4i4z$8X6j;A}%uB{5Su9RMCG{*Z;}!lG)jO`mf}xW^HX6Nz#$KEA$TRM%7={+mzoww~#q zbN&~Pd;YVL_Vx9d1(P`Z5i{U1na)SHYn%y<>2GO1%XXF|j9Pry$=f)5WdGczV@&)&Z0lKiKMhh~@NRn$CeyGK+E5$^uaF`3$(p|Me)TQFA>Nup;7oJ)FL3kySp$H z3I+6Dm(2eSSb6`YiAe^1VNFfV898G5Z$3#!*uSmhfA6^4@V}yqa$?uk*v{IX>OWfL z?USEgBES0o1d%_OHlU0M1RXFKd^T6rcJtRb5-2s(`?I+Vb%0<0rBnaSCy9FSyE8tk zng7SfiFKpDUS^Z>0YKDUMa5Vp`1Gu6CuxHGJR^Bp zZ_mXq{LultmS-vmNdK=@gx9&olLLGIW8w(VPyfD`m(#!E|GQUy{lEy}erEcy{I;qA zcC-3FZGrz!ba)fY)?2RZl{&C8D8_>!UYOklz?E%_Oi^2s)?w02Pq}K;obq2=Rm`;XxEzcF33O8%cipoqi^rt-Q z7H%;Y)*>>DoxN~TN|(+;)&-JT!9TTIe>dR)jm>P^r{=nHM9keV)BSq7O>Y_ZQha3u zzgA-{Nk&Wa3h0)XWDq_iH$GL7gOfQcCr-Cv_06kmwlX)a_shzLg`M(f9>N+Jt`4d*f# zp`yx5vJse()$Siwu7C5Gk0C6Ci^RyE^$AO41 z)jUB98+&eTuSY8@9+UN?xy+@1HunZN>l;jrIX*ug^<$8c9?yW^Gv@dsZk#nM7yLb) zm+Efc@^-fI3a|~nF#cFUP0G#BqNb$prL0v>#l22;hT0l)7b{&-bIuhr&h#Ydb*~Dr zMLE>LWUPxtQ}3JqE-#|A-Z$$UiR!DK=3f$BKg3A*%oJ^j%&pg+nu0@Jb>lt<^6~J0 z;W8O5o!Oq^s%jjm%I?01|G4=2`x_H2M{5wYv+SgzcPeXeXY>HJ%dq2WNpL8A^n+9d*IOuAFrxBR)2BgwbbS>^)e80MjVw zcTYOINR~eIu}nI%9N0xzxev|i=s9x zPF7YsIIS34#Km@L`DQC*90<7dCXZkPE7CxZedI8qR+tbkdpH0fD}E~I{hrn(KEYL$ z{o8mu8^;*B`SV+(jEj*uYl{x*vA*z&WP{nAa)HC*)tyrVZ27h?vnA&@du1%jpAY$9 zh9FCy1cMW;}-KhOJa9vUC836CV zG0Aae5-h1m1^6NlC7Ma%V~b0n>-^M7Zwj9Af4~#2IqQGju(9wHS=5dFGDp#S@E&LI z1BMCuUZmq`)xc6B{)LBnJ-+2Q8^Oby$anIThmJBWI_JW{V(vZl*XojVg@-gz6>Z7< z3Xhj)hcUu~k2dJFodUSl2fyFSVC;tb+*S6Unx>a=OQ*bE$RwN6D4dh9i@3q^!r@cL zRy%=riyn8CweevBT9`4zQxOx4h9bX4TcVPrOH5(~Dx;w)^z^oBVg%8Bt1gQ!eh>ps z1fg+I(QYfeYJD&y%1=cCAT-=Fi8;!=&Xt+U92AM2p{=f!E!87XpP7$VKVbLdV&sz9 z+WN=)cpltPw3|#lph{Y>>0@L124G6Xl}}mMRAZ5*L~~S7TZ8QgDQli-iH^!J|F>A% z!005f=FHSa*E2my_2*)+H#XYSS=@Xyt7=H$El7qlQ2%XzMi47mztoVI96R$fl+CC< zW}tj^Q;HZ&3PT|DtE#G?aJUu-1jnWi6Vc|8B7p)EM%%07HJ*dM)JclvIrun5Rz{0^ zkjCt}rh$VEZ}_^oO_GD0b65TShr{thW)kG9trz9S&CHi;RXk}IuscyoijWzTZZ*Yc zXKVpJICn|wI+(set>8wd13F$W zI3L~9c9#L`wfjX32!IZR(H?3g6(91=PZ^w^oeZ(&FxnxM($m=}U|oWiwXf%q-y25P zKRNhWT)e=~@!oF3Ph!8Up&Y;$mQ7Jg5+fRN%vvxT5W!>3mo=3^}P) znIXMWeb-ByN8=>EJL!flTa*45j8w!xuHz@{dittkyJfvc{OyGNNU1jq#h-8RTGl)h zDxff@mQ`PW453C8zL1yvxbi_brLV``xxYo&^Zt1Ai5uMr@ikogb(w|IXt1C0Eo0=D z64Z&x@d=W^Cvx!OU7!ub_;#!B5=2HlCgB1?aA9IV&0Iicq}|lOoOJCaFmMIzJf??# ztW#s1FJD@mW(DY}L{%4L+4Q*)B?oJ>YW9ekwyTzoX&ReLUu zFVw8~V;qJ0$LNvXavC)#o-{g&P1Zc2vB@cfd!a$7=+GjoB(p+~uKQl%gTaI+<81AS zg^cfp2@jfJl){+)wE=Y-X^8NAkihpX`Eg7@j=r0Yj5_wgV{efFqZQX>L9bt)YQO*C zss7>frN8X$H2#fTsIA|V?$4wO@j{WFot9yCuI{?iV@m66J1^$ll1U>$AX^$_W#!-K z@JAHR-=Fdd-cgkdR$wVqlfl=nFF-#t6}G+sR+VSwHNbXtT~vG7@pf-=AJltnB~NfM z!k|#9&{lR=-JHM;HgtHQp<{bw!L+T`8*K6AcB?^g@Lg5Ft;0|)xZ|~W+zvhp#^P`{ z#&^#oW}pAmc_>^^saPN#&NyL{C6{;xe67L2D#+>_nf>+CX5gJb;x~kvw#XnE9C=)2y%H(t0`T;moI66xml%csY8bJbs|I>RtiHl9X z!g?t#$>dy1Pu%tNdM|lUyvNv@auuz=4ncFdv0~J(sZrYF@t#2W!xT)%td>!uSW3BJKZlr`@Hd}KEdqECqMZIb*Aq<@KS(ub zk&yIA1)>IU1r2# zaUR$Y0$lx_+{$uUoVQ#U18(gd^g?{s*wEisV(F8ouy*k8O?z!=9~LH)f*cp?%tI-p zbr&Ow=!&ifYsuR`O!xImUuPTX+3_pVH|B@fyLjju1N&`$38f#19gj${N{8AeB$%gU z+~^?evOcF(_u|_1OuZRl>z<{J-cPnK8{O(LYwJ{U`730gOwm<)W`#1f0H>!Zd-LN{|_t`Y+P}l zC};AFksgd;y-_|>lM%`#&$y2yGGr~3I-l0uk+pQIxvglZw2F!PzNVOPav67%uIP=# z80(9mQZ|-OM01pu)}jH`;c~Uy`5A}UL8GVnNGDKB`IoH#CB@J7j7fT>ax8Zv6e6a4 zmLTA{k|S$5E8EMB$iVO_+<6k?fPZXVW+Y3uQp&AdM#|Wfl%KFVVNbW$Ge4=rt=(CR z%MtEfmWq1%RAiTA?nSPj`0N;%_OwrwGk%H1yKwP#hVwdXeAJ1hvpqzlX3zJCn(ddcA9a?$+JYe7aSfHe!kpk zHI~kYbsaA7K65yCGgTH*Ar#PthI1X|78Az$`OWEdQA45lIM(VjM?m7EKRz^tZal|` z7gIy8^W<{BX15fdWoqtUzBWvZvf3B~UR@4wN2_DoL+U7`wCH@>?wom}HI?-C83&&j zKM!NUGxi2)IJf0@?kiX2!zn6qLPF{sjS9c{>xM7#tRCNXee=ur{LROZr?CJ_3DkXZ zUC*8B?&$3Kt@q+gbv&W6Tp34t3Bob-$SDqHzFF?7wCB)C%K~@O3ojY&USk+W((1X#Z^raR z7qj2J3!p_nVJBwD{>%!wy71O?*7^iUn2$7K3dB>YLAG=6PP{BTqT;lY4KNHbnv61f zZPsU`+N~UCgEMzC*ENFs;yU%uAIVyfMGTlQduxBh#Li@32v6Xx!1Ld>s-m@JU4et_ z6E1oID)&6aAoG$8p-Xj_8KxSjS1vdeWc*hcPm52~4v5p%{f)@-BSJ)I52ue>OAW1+ zgTeRR)BUQ(6QRWn@d!J)7})spu-8j{Z)v2<*=fn5+j$&E_070#1v2V73=ahJ)^FO| zr*pgV#3{v8Q2NFR8JOvj(pZqIvX$#qi~oAB|8#z3^3+a%GjWc%S6lB{Y&VWjAzSod z$bKAH4qA>M8pTu}beHJC@l~6*^EhYj?cByryn*Z6!E(H0?*XXRmydv}xcEU;1lZIN z-do=DnF(JU`*^Bulh|j{Go90im7CgfG){l^+t<5V+2bMm0d=Td1!sqr%V#BDQ@Rqq zTb^6qsis>SD}n+lAWBtfQl*1* zQR$sP=v{gf2uMc}6p$hikQ#bOLg>8<(wlVY(g_fHhrqW{pXYtg`F;L;`|`Tj#LZ-8 z_RN}@b>Hh=X-ZOsl~g4v#qwgWyxnA{4M~vbEUmJv$QGFv@{Sx?ZEAA1?vKfwI(%{5 zWhO)hC&^4m;tnh_##HT9)1&TdO*({BC!keB511cwN+tAhr=6~7Xx$?ps+)+?I{mYg zxdbA|H=a_;zY+Dg!Vl5^15w|QYcV;|NAf5v0@!LntLp`-^&gj;p$!z&^o8Opzx|o(`2sv98F~ZC!ixywzVpz9qn!J8%*CWr0QUwy9RG1=TmNpWY9d<*pZL2t_HSg-2W4M@sCSV zqS>g+AHH(U{Q$}U1)@?$^SYqp-8K>~>Y931XS!2w^$i{ny@Qo^zgc{9?hMF?Ma{0;98-OJNTo3j+@pHdbS5pk_4++1uP1(m_H#=mtfxI1$r z&XS957K+|y2L7VzrUPSuaeNFUw0QG^Cv zil&)n-3QC~6_MT*#cDRQ>h?&hQm8or>(1=G;u#^07L}G_^Q7-F9{qRuli#YqMW{-j z7H|2tBLmQ_kh4$RcameZdqlHJzyD}54$HuNLK5xWG@F2S!(GpkrJ)N)c^81}KFliy zJyM9Om9Z4Db!kx_di!!?a6=*DdyMDvVt_o@cGC5=gLt{^h-64DI=)W;HM4vTw`~q5 zzJFbC+ym))IRA9d0i3+mWG~?oh0fN4k0V_^e|%NCku>zByQN7zy(^z{oU}aqH=OP9 zGI0dOjui9O6~p#?#*Wi?8E-7Ftf+&*;0tH!ehZK|Bn7*8t?tb8HMT_|juX518t+$D z3oH;1uSST3PJ9p$wpm$O(T+RhzZdV9iGTL&RfgOsR*P}=O?)o7vu5=|-h`C1GfOsr zr8zyx{W{61$A|nn`3M}SkcUoMdR94^l6R%JOqP(VI+EGPq|A3$HoPn|NyV z?O2;3Rq|o36CYExL5MV?OrQd1oWlNl(@!j$Kj4x=tnEx*l`kC1s~gM&Us9ZI<1dki z$+0n#f7Blua@M?8_I1daJ-t#=awqN6U8d>|d+tDbxn~`0panNn(GUMJTo(sGN>mrB z>*xJu4#`3G7ou!+WtQlyJ0##Q`_rAP$-+i8XPn!ytZP+3*VIr3ef-ETCWa0VCkApR zQ-~yiTc{js9?6X@kZ}~4{c|K;5K7{=abIspSY;Hbv{{+=ycXveq3Dxu`ykAPO;-i} zw45})|EHaF#I@)bK&E@b;PxdrPyU)ZCS0vs7K8A^glwDsxlM znyQXsSr0l#i~%lj&E3Dg|4qW8A%29nmDbE>tjha~u#RWpMLAh!)%ie){BQm7pL%{7BLqVEai4xZG=~{{p8CY#nP>{a$g%Ju7GZ1NiA`n5TwL?g?li zPs7EwnAOO51IX{RdHff(Y+&oo{!Nlo=r4+m6{uqRDldQ7Oa~yk_sr(YL7^0)b^+O&|5E_; zFVN{f$t5C!0(7d*IWN54YXp5(B>?96_7c$3=dfC@Q0twZ9f>oO?u7f_TFB{d*yUCz zT)p&e<&%Lx>Pw-pn1O7&BBQ?wgGbXJ}y_+W?BV^J~m>v}TDfs2fihHQ%@H&uzFU9{IwiH*Nx8 zKwSUsZb>#AzVip6?*EHWXBF9waN@S(>bu-8G<)Q-va*hdO*E`+Z9Cm>NGkssB5x6P z<7G_uqs|czD|l7+3kI#l^*KYJy0b|5A4P{CBzljqv|JyC>ZFm&#!G%HH1oi11J$W6v>!Izx z!@ytENjNzwv$C>MROYhz#|x3JKi5P0f8QG3|Ji*;qxZk4zMh(%o?hJxJm>##5w;r^ zoEJo{K4b>^>mhzXK!9`gBV!(w};8 zcoem}Gs<^12R17=EOMs_xnT<;RyN%O{EsB$|D=(YKrvNA13 zMn={rPku(GH_i$_Nd8>{o|b9j0#V`nMoM z5KwPpsDSC!OBsK~csA275N=e-R0i4oe+ztYQDDFxwOqsUpSIxDv;o7qfiidG$u()_ zKi~W7l?>oA9JUgZCgT$R{hj`3I2x_=dw5=H(>19BmWA#-pw<8*1IWzTKQS?({^p@x zD9q$A4uOjix@ycpfAen@8iwcHsu$d<0V5R256rCwVQuWcUt9ormaHtNt6?Pl z%fl2fBDv8+pSk{RqQFgHR1_%N*xG8Us{?vj?{ibwwY^FHX>P%Ti?;!Z>6E?lPdkix zFV>pXC*x{_pOybKt-<2OMbmTy=!yUBl7~H`-&Ny(&F<`f5q!~9 zFslI7zgFP?Z`%!w%gEyR_ySYzc5@ye{3C?05-R$1%?l5YXXnU!1%qk-v+|xFMx5td zup!&r?d;vJ4<$#6#n`M>iZv3K~c;5w#-CbStA|I5R|9jm7`kv?OQ0i*-#EW;kj=`_U@6)J{EJs$!BO%+nk2Ia! z<><2`B~mTlK;NYbMrNlL(4L4795j=KG?RsK)ICBPKVijOV|~(feQ0DLzhE(xmX#A6 zZFUe!_6W#-{JMO6Pvv|Uz~ul#!jT48&kM0b=mr;_9hbT$=ptQf%7h#r0aM^3 zkOR1OyMyiT7vy|ut@B6!a@?i?Z0g9IV|@=tTJ4j?5p+JCjoT#sm~hH?T|_gPlEJPi zcf7f>&SCnsKdJCGx95&b@LHH?k+yu#hS=pShDwBn^vgAxL#$0*B@GU|$Ygsq#CLK! zM;PGA%w*2{q6a>O2Cd;xrpzUUP7d1}zE|#l{>2rqt^hDTF0P;2fSta-vO&wGt(#sJ zUJlZ$I`m3oT_aBR%$Lr~uN(XUnyvom=~X&CmEfE~a7>FT_mebxm-@J`8B``0dl!XV zU#6uZ2r%lJ_oSnjFNo!>n3B?5~=lMp?ZwUUcu3kvlk> zh+$XzGFrv2!P6DwY@;h*E@%o23G%l)C=r?pmPl#m98&&aeh_gN#p?Xq!rhf zl$i-or9?Wir#I$Eye4bjZ19&RM6;qxs9OFk?*4ng$|8vzy|bs1t<0j~a|qIx5-}`H zYNg_Hos6y!x~=-Me_KY%8HcF~N_n6JCe<)!eV7l|GgK)@9>9jvJZ&3AT4>!nd8GQ_u|-3ZwI#xEX2X9^*@S?y;e!E@w3QPF`8aclOpytNr7q4$4l4t@~!N( z=})B&V9~nAZ(S9|XjK{qPNBon*fOT|7R|#6v|9a#KQ`}2h~-i z)ble11ci+fJ70;Ja7ca}^*SSPcwqiH>?CZ_PMzasX zM=ZwV|GeHy(@25!Jqi zvV)5q&+EyY#qgC?$# zE1>nDIwq=D08jy(Ksp(NdZP?K6CW&N6wz)HaBGNw>2&_9x0`jIx1<#_NolbC#0!y?oxtPJJS(g@w^ zNSNNFoXV2KKvT+{+EE)llgaTR=h+HMlsrFgUw*zLQ$9WN%a?)`+fc?pzNjSXr;fuX z*zhbPACj|7uPeQ?Q@6jkGvrQIM=vUkJzTBd7 ze)@TT-?q#x>x^iWmiOb7*Yu-6TGWmWSc)z%`2_GX`4YVXj!(d4i1*L_9LPm1Yr^v& z^Zf`{c~{qfuM$&!Ud<~<4l+^(LSKB&zP)8<54fxO0h zVh+BSOdM8n9o=L)ft=5ng#|9wH~aqjwBm*x>`4|Wb+OE11*qJ8``#f0{l^<8{P7Q> zB@k3auSb9cQ;@=Mu7xYf@Im!>vR^SO8O0cI;y8D-xxiPRkjn7iwS0Oht`i`V0mk(A zp++)9o1LF3$%n!p6n$M~a)f^*tFCV>wF)b?{hj3YCnXzmX(WGwb6brF`?Y-ZLeanAEETVb7v*LoHq6_(aB<`l*L9rlw;LIPo|qVCge2~=A;~b z-0PBHS2=JmiW$Ld246wG4R`O@n>w()lgNMH;`W2s*65?+G$&Bh^XHHLQ-%Mvu&IAH z|381LN3ma`qFxZ@cW`=MsTw!P-;#YQX8=|@RIyfhmcH4RFeSe2^@bH>@Z_1 z;VMrwlfSTKsca;}gzvf&aVhz2xq+tjdhDv0r!npTXCXQM$4lYg?f?F=KUp)mW|TB3 z#dX?FKtf4u(tutx)5~Bo7-2H-!ES>_jXgGrt1PE0%AQXxaD;zaKX+JgN^9VQN^Od- zhmY?oE&cOMeFJ*WPV0&Lo~*Y)@~_Q_+=nmP%F1$oEe;D6(M(7Sx*OU8(zG$`6#T=( z`*jUK`z2Xzvsf9ANbf1PLpBO~Xz~S5+kOR1#hey^m%d#WzocC=>Ts zKaC8Mj+P;g>8LWS#EeFI*AaBfv$C_tptE(lKeA>zsb=FXR*~_=L-X+P#JWF_OSaJ_u306ZP;Ts&iL7DtkLSMNLi3iv<|C{I8k^;@!_t zb$Jo*;CY!qAtS?J<&-H$!?DDsl8h^(^}sR5R){~?aMCJ{0UDzNO?rIbJ~<+*R6Xl5 zKu@v(EWW&k>UrCcDviKe64&13!`<_qk=~U<2h`dgx@e?ap0W=Nn^d;q&XrZQ7QXz1 z^1h~Qslo~UPd}p!b>P~z)2s=Ao4@*j!S>$)3W6Mbx$=Hq^T#=SMX9o95aVz#i z4!=GtFp<(f+;pa-qS64i?5Doog6E1 z5M*^hXtoa!g!#G~kfbZ~Yl1s6KV23#l6NuIh_@FHHUeciNApn|?S6U-*$XYuO|T`~ z_LzU5JY`m?*XvWy^vZ3>&b^a#nqGKzD9-4^;6#6B$Ls^5T~})>9u`6us2$Zror!ZQ zSmh>aH&nhlR~kxd93A|W;kSkOeK33&#+odo4`(~Le#vu+L^?bz>szhx;w541f~cq z^<&?Nx*e~-D`(zP`K4a+z~86S28(S$z}3(YifZ$fv{d0^TRm%!%EXTO9ha=hsf1Bz zm9Ch(ye3{{87UmVzXgAwdyjX8%C5rB5J3#HQVmLJ4DO(G70MfVfp+l=KcUx9Y#DZ$ zwk$HWBS#C57O|~DphLBJf-7h?Upn^!d(Btz zo~DdG2y8M5w6x&k<;@uNZY_S@#Y#pUy@AmXGdS%POgH{Y?>}@CpP6sg3km(Ol|e{=WIVt!>A5qBIGcV-hR8haVtnzo_2+*TtQGKs&l@IdS;;xRZg-R4jUJf7dJ1|XXBSl;eLFnA=Fl@ z?#uy~Iz+ z_lS?e;KecKMKP$ntXJr2HUZW(rPmf~X$}?AAZ@;S4Isb*1nSFlvrPDcDkiP+8^}5r z)XQmi#?!I z=k5~V0ofx29CwQX+c(8e)_CR83x81pug&LZAub^?aUkChx9a^d0BysR1Ah_0FZ`hI zbLQh{vS^*xP#_)hxMsqOU><7fzBXCOcdFWj+KPZ2`>=DOoj{^IY~XyP_X86j4-W!i zENcQBr^tAr$Qq=&o7bzLzB}qdd})#o)4u}gkJF1f2+18m@oqE+xP-P97jQNagoTfw`$CyZ^1@YA2nhHlf1!b^6Tf0_h~^(aqAy& zHO1%`t+vG;)Rtm_kOPT2#pU+0b&a<{*Z1}y?!zO+e-3^RumTUk7byW68vKc=g;J}k zs=PlITMGexn<&8LskKk-#EiP6dth#>foKC4b{@6=>zEcSb}aqbRgp{BF2RX2%@fc_z zlvmRtO^#s4C(lvetm@4WI;jWAlJ8&$1r+1b_OJohAKH@LEg1cA5qe>q+(&<@A^Wyx zZoUgU6MMq$M1MeGJcQV4_3Kup))S_$<}9OmcSE0buWGDRouS`Tkxy6%@IM?eelo2# zAGT4a5FM2N6;$L?&A!OD5c&yRP8V)(Z2r^67q@DJN=h-!E zMR=7-gUz2$);sFAjgC5Lf7Hcxp74ogyPFUJQHV zN42z+z96=BGH11pmm4~Xz^LN3J$OWT)7JIM`6Bt4)s3wrXNFm&S@1k&U4=`(7586R z{mnpbpj4B^E5Lj>9<<%`_m+bsP6?NY!n{o01X7YDt3yf^jHVRi-^&CIiWEvfdXD2$ zX%F^o8U(X{fy}MlD5BdR+j>B4D5(am1QVpVGrx)FKfYNX_*yMGLn{Huyb~dFq4?FU z&cLeEiFH18DP9}i_3Q6Z6xDR^r9WZ{2x+-z`Ix8Olq0$}TMZETjxm*eYp3c{S3#qp zo3$wI)lb)BLL|PX@Kq}tS*Wwmo~V26gwBfZwXat(rShpfx;;5D6y^GB@ELR(C%-=P z-F%BzC4xzJT=NTO_VFMtdU9W`|FbtvY-RO;<#uqpx~Uf5U1(N`ep!O&GfFHXcqyJ> zZ_jhdD_Tzd&1mD67cwu!Fzi|ej~)tAPzX|Kot3#m);TpVi@nKB1@h}PYpyTuT35B# z%#JDdQ)-TlPgSX{#chXO<0IGMod3$s$eLe-%KMf!NzT@K+j|U-A3@7@I|Lxuuu!t8 zLvj42rDLF&1!|e%=dh~terA|N>eu$i#pxj(>D#P{D^%1)%&lC!+4EHza@}bkOy4t* z-_u-fKnxJ#T8F;xa8atO@(AUjYEwq$l5VY98^M_3RFFq5&3tJe*J(Xk-}A(yZG7j5 zu*x^Qwafy}Y&S2I?aMJQM0RMQ zgR-SHRbO>4PoLwai`UaO+nHgRJ_)dcD^Hn#T@m1Ae);H9g9U&8fi}xCdxbD%8XgB? zJv(<~rJbyp+^bKL4KG&CC6BJHO&Bg z-lQ@5R{q^l2;-S8FXGblXV$e>7_%a#mYDAS?(1}`izC5GRq&KBqOp)P;$sa@InE|?*}c5mOhp?E^{*Tv2{IZVMpS{3Oqvrc*$ zpytuf+v_7{@2OYqqIgPNRTt~k_Ldc?Z7%PdF9ZA&8#>to+!_JyfqR=R5D%s}-jlu@ z2Br4ZBju^IyYW13zfu4voP|2I#?6f2bjm&Pft(Tg&K`?o@pWy^kY-aB`tbCbg3nN7 zUAAJzi$~qP@5wZ}%v0#Z@3>RuVLc5$y6~(;S2J7F5U##!&9Dr;`B-k8W*?KddN42e zv$s$3<%>)8L81PmA1K? zW56Tz6|l|SN<}6{H=<9BWTc<3USG$2lQn(%@muwXMhHPm`u4<$Zor#o;Aj?vpoux2yNNF)_jy79E@RkW)017@apL2hDpG`s+aX9RrnN>l0X(bO}W2mzZ5Ia zLrR?CWqX_OrF|`ETbK2*dB(M@6i@i1bkkHzBbr0QSYfMjL#$`Jb|qfg;(m{6(%exbXH zqxJh&;JasDORnn^&yqzBQl`94DrRkUSN+8DXOsA}KT6bnEjTP|rjrUdo>iDLmEx&i zN~yy{tiI$C$)v{J3#p?|1 zLtzC^kvSu2FFwu#BHgaSkmX2?>bUHu4+gNVL@5KW$LS4yc8nR~8i1O8{M{S!*!hU_Hy)%HFPIHHa2Y zg?D*=`Pa7q;kTQ{bxn9hEx$9rJU9h#jN`>b#}wN;Gl`CM9uCvjYU~=V;Ya=q)&i;L zaY=&$(9W_Thg?#UYUr^dBv@Cu1XYOCR@cV4JLZTWNE!wkUWAp@u#6M>#XICX^lg<% zL+|zEoUf0ttbS5DCeuQPFb1|tM{R#$;)nUFgf~}eH>v=7*Yyf-8=ndD1G z7;i+Sx24;(JxAQD4i=My&>z2E5W%m45r9}kyPJ#5mTz{}3ElDi!R)8bz7WRG_Bl1!dgR?)!h_nB9~2EP z^?FKV%>+^ z5+_y}lmXA{hNR@wyCW=rTnwtEFLTjTPs-VfK7R>fR+LlJk97LZj?(FW86nET|0vpK zpH54mR;llcxQ|Pu(yMrZcjRNLDN&pDGU z%dh|j`*Q%a!h0BS0XMyC*oF-Qm{C+FfbS8VC`(J?{gh?#yFsahjV>(^Wzi`4I_Q1O z!Oy9LG|X+Gca)nx{a8iH)7(3_LDZ?0$dpxA`y5Wp&-q<1vxOc@c@puIlyrJa0xl{> zqfx(F*F^*=8b^WjYxG*jPd$GbyhV!Ki$>UuxUe#@f2ded$+l=#8g)DDZv61{XqqN; zOn55N=TWE;NG)H3h8yBLn#^U1Yx|J{f{Ie$cruOvR-dPJ4ztPVa-_xjIg{W<>L`P;VdzYlH_i$dRJq5h7t0xodb(su|`Jo3V zKZ{rm4h%;;@@PUMp(^w&&=lunp@X2s@){6YW(stK%ilF{)-+`aFN{HWc2yfjBres< zmiB0b1PTw4pSK|Hg!RbpOCj3U2WP7M*NMypAsVhW>M42S+q*HVdjr^;VPS%so`%dU zIC=&#tO7@741ZE&vF}+8OVKmemCX-%6AJ8OSEbYfEv3~7A=QQw=;4KEqaF=7xw4J9 z*usv@qQ8AJViri__42t%G|Q7Gzk*-k;iW_)0;RnJ0|PH~EB-{llP7N!RHOw4k}@mj zo(R7~o0^-yjGz_kaKHbwWR2;4yW8vQ^t5Foh;IqU!&QsIkZDD4O3`0b(KhZ>d7j{C zdQ5~?vAjZNjNBkav-&{G7rFeZx*eoK)M$|bR3OoYQm=H};`T2l(nOu+Q?8&!N~QQv zmd8dLO$*FJW>Jc1F)j5P@3^}=DpCt-*T25m(X|ajc8hJdpxFghG*$PCY_z7nWPaG+ zx6!n0Q)@L8(xZ>pSIUo@y;@&SHmrIW#0+MUkuEaQ&D^aWj?wkoVJ01UAU_rV#s6}#de9|dyv_cZvT}m0?~n2DdSRn>S{fSB zfWr!=V3LMp)d*0=!+emc>|Uu`AdAINSYUL#`?)C9RCOn#kNceDrH1u&Ee=~P(!Q05 zaR)q>JRTv`Ji8gX+x2!9X6h9tKan9n67`JIBYP&zl9G0NSwsgk;--yn1NPkZNp4ne zeeiNF+fJUFs=qB$t_*3)a#v1HC?EXndDLec(k9vfgn!hvfRMJg^MR#B?A;$jG{MCN zhAQ4RlliTr>bA`}-f|BPI$Byjw}&2WJnersM?_gcOLvg74k6Ftc=G*Lwk#V5FQ)($ zs+X17?r&Q7b*2S8N>nzvJf^4LoBKT8(MEfe`u(ONC`6%3;!jWG!-VL|;$l~D;5ycA19CT;RfY$0wa z+|SIqI&6FTh~&#F>xl`sl71kD`Cd)zC8}8&zCp-CW1_;P$)Rf8%$XpQ$gi#MqKiiGG~PQ*;Ojx_I3ty9=-%+{{K>Z8ksayRU1e{@$f{M zCC*QaGmlszqSUnU3Ou05^sdaA_V3bHakb*lh_hffr%!&RcuJF!T0!q6P8DQj4|NT1 z=Vk&VaQv*cy2^mv(w_S6y)7M41x>ks)g9r}6< z7J>$38J`rjvo{SEt!c7-_F&1=g2;gzcVBAIU!nXI&RCb9J9YRoH+4T!qZYiLMT3ZH zFy6^s7^UEnn#nO&Hl*w;@RP2_J|iQ)HOm#Z0m^#4@ZxqTIigPsIT|eP<5O*zTXd#N6kBFwa+YTEbXOycpq;4C_Jnx#YK!cEM`1x=Mlokq{zt92IY#d zTjGS?ue*C*X{s|)z5I!z{Xe`TJiKB5#>@WQZ4OU0dd+7i*6!ph;FOjIt@z|0iGEdA zjATa^#>u}TXBA{g)3J7Sm3@TOa@G}?-^>0?{X~wpw#()Nvp3~Uz%CTgm& zUTc-!duydCzEO~^&htToa;vw^fmi-4XmLp?;-u5V&6zK>j|4-8tSZw_N%`(0G3o=Z zs}hVOUE(F(&R!)g%v2|C`nnYOzFotw+sc*QVBP|%oB3g_s2NB*niT(DJIYHZM>cG4 zf%4NdMQ_kxa>-;hrH!*;ambqRplWtvN>_JqC<rtmNZJsRR4Q_;Exe-*UNFX?VmsU@@SIAZurYs z#pku}FCbI94BtzDRc=Z&lF2&xU1{F)*pv;eCqL}KKG_ak)}RvF{+@TM@`wP&Vr}Te zlE~u5aMPES5AYMqR$AmZtRJmiwk<_2gXOUmGO1T268ka7RJ{_%ne{-dW-@v|_@J?f zCmyOj@W8op=yX5rLwno>T*938&MiK?)=OR>(ZGUHz%BgNk1GM7!#h<^2bf)2fW+1a3NaJ1-;Q{d_F%%9amZ2omHF6*OlS`afVA!y{`SJ)crO(Z~Y3 zpSUcC!$e>kfrxLv1c8ubeUGWza=sU`hMeN`=>Ocpr76JLiN8yv4aq_4g6s6s`~bhs z*q|;reSFL1DGO6CmW^L6q!*UI|L9%)K>xLy_H$*O5q zjsTflle!cZc1+GK6oKUzK#%X$5HFc?x(NbF*_GITzO0$XpbCtX!USH`)UL=q*w@Im z(fbl&xms$_MDQxBtw7IcViT2xkZ$IUUcs#NnfHtH>UFX(;r)#I&6c&pBfN=c=t_=m z1qp6vUB4AoS=h#VBs0)ksiE$ojn?e+Q@%I0k1{z?l{XcdrBjcSz>(tkdvMp$j$o8( zSeVFyoTrWOEZq<6>J@8o`gRavB@n+N)P9IX#X}*=FI5kLnIA$~iKPX7VG9c0{Rxtc ze!zV0mwht5i_q`l4ZUN(m|dr>L1>SzNthIt?NLQLN8ICi<=tQkIibI3bY zIYqC1Ne9$K8xd3-s)g>k?&08hmqO-Wc*{n&A}CkbJNLO?&4QeVid{$kC6%C^)EXW9 zdm)!rUBvQnNnOFE&y`+_u`F%G_(?j{Z|ylEtkt10Wo;Dk)cx+(D!$PPy6$ifB0NI} zzv$md+zc&30|Nnx7zHIIC7#4?fHA82{P{Zo@rVt%iMM%5zS@vzIaD(cH2{}K9;}w| zdbn*#m8J-%bx>2r#PU-|^CGOVHgso3r~1}76aKiRVaC##pJ@w~GoiLXR_=1#m#{%B ztNJQXND<0>X!Y}<*KTV*9VgCkbt1o1dR@pvy}HOyF5d8bXp0?NDRQvfrs*=*# z%Er!noea4r>#}%O6DR=0*CdVUJ>!U`3{O_~ejBKX@0$DQWU)DjRTon98a(Xy6y;^$ z_?=&jCD&%C9XIOd+rCc;DG`~ym)pBnv*^;F`rOqs&^bvc+wQ5KhDU(#?7A5_n%Sj- zRtf2aE1@jjm?X+=TX63C`XG@IAYtP9FI}vh6dGTsTc?LHBUB|~8jpMQvlq>1JXcj~ zw^R=rS!=hZtocC}@*U8eJgn=i_gWN#hWu=KMcc!i`KN*uEk1dw;rqqH69v3xcSmb{ z;D!>oJYCe?=+tVG_G=n%G( zq2(5;-K@R(`>_je258AQv|8KmL9^OJ;)ZJS!p$IZD5a3n0x6j(UvR$Qnl5*xVNo%l z;|^0l{dHAC&3Rj*v&$jPAndx1@lKwwX6Cu-cJ;uTnViP$;f;1j+CA$5?-`itl>a707N-;|Ao^3p9?yBH(4f&F9+#WvSO4lFr=yaHeTqoz%(=^oN z9PeQTyetVceS%FMbcZhZjRaA-ccuZ}3yUl7n+ z+>wqV8*WPl)4z+%G=;|M0Axw%kj&iNUo25q$d3^^j?}bAwK<%vj25&=%gj?Hg7|FI zb>hixd^a!CeT^!y^yA2D+N%*?GT*m&VMe)f&`uYO$gKeZ0+;}zx#p&)HBFVU*}g=l zVRbYghpy5s3kj>nuen7l`Zo35yGt}ePLhBuM7#u^T<1XQw*MW0o7V?LIR4=R0E{+( zg#Jxl>>l1jqg>7EA>C5qo%a;von_TpWi`s&hM9yX>*OiUy{QGV+gL+d@}p zG?7Lar9gq&H~X2#_>XPu6jR(-9SpL_i_n!08kvM%Xx4teZeaoxToBReZw;sRU}ch3 zS1Z)4wngjWFna8I_8G&T`(#k9oU%7VmIWZ~TvjhMa;?<%mN|B0=Qt^Pl#Ly&?qeIY zYJDw>+>PJiw4DSm@$PT?Aevr)hu0=&5=cq4L-T#?8eO$(xt~-Y!ZL(GDW<-*=Zo+) z5+FI)BY>6${J6wGaS!+ruXO0`x!;xJZ9JsOn_}ld-o>_ zdq<}ZUchYw@44>W!yVbp|B*r9J5b7)EsJA zVrIR?=<@^9S%4caG3%wpj+d*PPjKH96_f0VJmcOLSKq{`{6C>vdw zEcq?f0Opz!XE5MwP?f3-E7^YZGh57S-+H>v$9ZkEuwSGw{u(T2XZBj{4n`?}B4XJc zEYXK=xsUeFGw)tL))u7;=nr?P^8RN1wzis6$aQl-=C~V%s~W6RTQalpR>>SH@AGh?I)m)*mXmOm zGwi2zUso`GuYtyVOS}@F>2Ff-Lpfjt`A?J>&?g_>7<_)pM13SkG$E9KsQb>Rm-g9G zPa6dDnb?=lNNZVW0FmKCmpkD4II&yDU`;VDb#1U@BYezAtSoZy43x(VTb{_~sLM5* z*ms*;wpGztt9Db)ck7DKuW-Ty6(uAu%wK~T3Y(qkTEe~a6o3T3GCTF>XGJOcqDUpL z8wKpk>R$TXGi@6Vt7Cx~1P5tiUepO9JMTEl>yacI^8Fi-#r_9-$ibBD>UcIzR5j&^d9Afr192Nlj^ky!W3Ep6;?ot` zjP}Lrk&|`wKEhtYIm{~Fgvu(mo}Rknxgztj1I5M{w`Ey61G_--!7Q&yRn>t3`9#}k z3kjqGxEJa0NKRZ_oMX;zzfs^bKI1C!D0cr=w>g~uY(Cig8z#`la)|ch3+aICK%8J! zg^%s7r@*+Vn3(xlSCC;?lM)pS7~TJ~UP9rFmepaB5rkeIqh^uc`DV zYe7wVRjt=he}8%aJ_+)*>hAsA5kv2Q4kf&0+ZGzt<&-w9@2%CdPks^Ui zwV6jqGM)DvOa6gDbf$xZ%H1% z7JhSuSE#4or7GNcG-V+vh2KCKtf*}cS($Y-)cY(uKmcWfLUiRNNTQP^|om09+KkwbNZ@52#!p1mN)wh@ra9ZneD3BeYP-6RC*jHHwJ`GMU zg(;{cVKMZ1j!l3?q|RR(YIP42Kaiz!UxUuQXEQMv=>Z|<1{NHC$bNjMU-c_obA-+e2Z)DaI>D+UQe$A?178N>s z8R6iY8{oBc^AUx}Mps0xP5_44N7n{}8&}Zb7m@HpIL`PEJrI<7tvk|FlTd<_xhsBv z5dnO-)~eba>eFG(tLNp;d0Zn}!y`-mZ+tV&Adr<$8}=z_Ro0VpgYAJZk&XM)f8!AW z@rXbUW6bmU=x|Myz&ox6{ZHeZQ!V#uCDQ9e1YO~d%2%e($NNuwr{73(HZj8(F@ndw z4f&Oo&t9EQ3Rjn$hWKiGBHi=_h3`fA%lN|I!5$~9R%g^zKEv&u^u`h1+|aN1l8~df zr08}$`aXN_J4DAzLcdFknW?zd0Ji)S9x69oeC+!R9{6ENr3zQwoViF(BjV|S`FZLz zH`}Npv%B;T#y2to7x8g9#^g^w#ljOe^dr@fTm!NpCHamn%hC*w{SN42?_F$MWEs+3 ztgAbfZEJe76qX{8zo7CoYlo^^n5F2Ye#OyZO154FhLHu564x8yC9RCnt$XdL*4XUs zXD1&w_B71vB=-jzV5P2C#l>1d6Iy;J`BaeTln-7f;?5^1 z$b23Fj$5a9&)-Ay^t{0pybXg*me1zoYZ}3#d{2(-affq~`X+*t@@qCmLelPzxo(NS zZSm2M;oi$5YcRmJ=VQypom{>T%V9)3lhV8PXz4 z{@axdON|rQalpPW(8ZCCers-Zg$L*+@hz2R$docW%mVSHNy6v$k8CZJBm|F+D-gUg z5i3?5e#GQt+93DMrYv{gGc$PX&c<^Ck@~N(VhB3Fl^B=a**gU*%Yhs@90}(^uq!cM;cH0I~1`#H-r}_QkrT-NsV~m{b+_JcA@y zl=a4QpWTxRne&KKX|UsY4(zc@)f@39GXBbn(KNRVvtLd*4cj5az7>Zz4s;+!BITkR zrz9h>cjsizHZz1JoH)DB9VZ9!MEr0&{b28s%@4=BhllEyW|!40C-}{vI}?mJlbOg- zB9vl2#%tsicD!(t1!Z8#IoUT$p2h}aC&N#fhQ#dbUp3GbwvX`6g9-O?7n7d6_Q~vM&|!rbTT&?j)$XQQs%F%WeJ5x1tWug{FnYv(<= zuU!0lults}5=hISpV#Gh)$lgo>nZuQVNIr))&<=z<+H`~r0WrL>q29B(Z||?S?9e| zxaOD8rK5AdLN4u!yQn5X)qRZq@;6bqy`Xa0g0i9k?v*~ zhMIi_eEWI-zI%4R-`#U||Jbwl89fhAUUyx0T-WP$9cb9~2U>!o#>rIcTVgIJi$5;R zaw{7%Shffu%u(9Y4jkUhC~Kpa+P+pZI69dH^BV9nOX)Eb&5o6ktxK{=Yd|^neARSC zvmc9&(B)I9XYWyX%^wWOxC*ds6V`TdHPG_1qQC1LCYo3G6&*-bzP$gvKx4gF*+S$< zD9Op(<+U`IP|r%iPC*7y>za1jCh|mW)!I`U)EKz$O zh{>0j4qgjmNxJGWx4Z=GT9x(X*3vJo45R@6f|-)W^ffnh57=_!YGAfX(0OMzAXKA| zP@2Di7_x8bDrdK=huBu+6Ps7K4G~nNm!>3jG?rP$1t9-j^#hEkvpd*rBmjV%yo`sO zDiLfFN2q#S_+h2|Vd#dgn|jks7*THzKlR0z%wP(EA#nXM@3sESp*!z+fmq)y9*K&A z70Hp%bO(hLJ2#oDDeD7q3o5yjQxP)}cA>JY&%)g2&GJ{XW(OX1WJJ`h(Q zK`%7P4P7z96nOi+PW9UGNvn*2IY1?}&+PF#3N*+O(w)Yq?qV(lOGv8U68r)}Faqb{|3152^;MR)%Q=lbn(@ zw?(k^ls$Pu(0&q1hn$5$4rTW8_o{azD7=ql8VC(6d_Fyrw~1C35TNoKc9OR%a?=uK zjE_69uae{<{E&5G%;kH@OwB8QETFlx0sw++yC`3Wy6(ByUT6I}a65CoDU!J_BKf-9 zN!?y=pHnYuwb*gzt8tcIHuH8fkqnJBF=5vp26obxjiwee)ar1s$uBih2MRWQ_s0#h|697r$W%0Q_Y**x5VW$p{MW z-6DpGscxXuYI{NyH z9J`)i#pp?Dh~}2p`vNDQneLXC8CP1Z59V5G3yjeH`Z`J(B9*NycdS#&aG*F$QZdp^ z*fgAxTJmJ8wHnd)748^v-Zy+Az@DV0CR#>Pu0A^nQiUC?%=!>x3-1?U_7O%Q^0J^o z1$vX6CR%BRaj9UFWm~2WbKSAuAMIT#GZd|EM_>M#cnf?hT8WF!2V#A_edbk{*yEga z1*=b=*Or36d~(BnFJzx`tQzK_>r}j9F1SPvlI?n&@>bXtKx-jw9d|RvsOJsiGyP49 z;PkHLDcT3NdLb-|wtM@#O2;Oy?E_=+%(;|gQTaPIORsk&KJ@7pId^ot74{iGa0GR8 zl(pOsX?ft_9s)^#-Pza{DDLmSL9i)|?RM}@M;*-zVeRoi!p*TXwjG<^3@Ihn>6CQcVh|A3RkMwOd$oEj`9^(CDK+N0Y})FLg$jKhICxwz=m?F_A zR4IF~Zy?+8SGLW<3ysUt*+!AKB70Tae@`4y|JHauC}^L*5(S#){{*p5z&I4FP`zNh4!!tKCsP93abZsENOkGmS1 zV5>wdeyAl>)6)j4AB2^4x9#>AFqaBd=8exOD`tP9q@W8%lFB4)HZq1FV@GjrwLjD3 zaEBKni+vmU?o@9W(s!80cCV`rx)D_>B?-U87lF3xzVA(dszUosb2p@IUD#*oJ8E-ULGuYA-vb|sKP3_ z+!h_W<K%I7u@Iso zdsp}JPGi-t3CC?Nd-7&XeeidD2d_+Zn;doN3Y8StOl{SD#^?64IlBdx9jU~mqmBX~ z1=eAHN89=u=Cg8a5sw#q_I}5)ZG zFP(q=(IcZ}UDZa`Jf=Og$RWlMa?m*%Eo`Wf`{oVQb$8Yy-Mjnqj6?T0wZRPIId1Rm zFySbf3l~MXE9zVy)0a1l81qC&U1-!992raECBFCjRjz=zSXg)J<2~{BWGGOkq zh8Ea*HU!_v{`z!H2ZOIh#Y(8r1m*ZFnSk3avrNiG(VD^iShFfSq|gEodDziCzt1?e ziUX66&A$p35ZYThnzNzyrHHA`HVW-}@YTMwVo`rOh0|xwTNZ7X1NyeEoIEPRpZfHbp?o> zcq6%W_+q`r@GivWjK9ZUNWml2=WwlR%Aw)dt?>HJSYl%0RX_X*JeLdSWLx7L*>kYM ze(qJ1LxIng6+B1X@Imw*0uczrWv}GcaXPmz_(H7^}r4Qg(ha+KOVtm$grY8i$r zf|RAcrP-T6XnH5)AY&DoNjgS4kMDq7biQ_j?P%76(P~or@Tbwy%tKn=)~Am%9DGSq zR6u+1@;w`q{RAHD?VgN66H5D+ISS+RiX(9@ANdq&Vo9ccEKyap6zz<1$bc+hwF$CCn^ zmzRe#kvmq?lw7YIcut(}e}UG(mY?XaW8XGEM^$_jAFJXy_U>uhXqxKE9fq&}z;1>f zHq3vsRG7PJB3=@6qNVGKdOLutre25Wj#aV)T0Xo3^J2N)2WPg56}?GMnr+asz*qNn zBLEW(uXQNqGHx9i(cK?+Xdp4EhYFyS@QQDfI`}yBvhD4UsvZHfkfp)0DH{2NY8I(x zEI0OzPhPJ_3*xIbhVR%z+x=t&w)10ljp$Txp%Gf)peE^b{WwK1zb2(wxi zj`sdyjNW6N^b_Ce77(*>{{iY!=Rq}EtZ8KJfF_5J8G0Y|zqbwPeuB!Y99x-bz@)6J zPK$ZukA3gmoxx^c_8FLzSWMhyF2J86She;lfN7QQaC`>(5Rs_)7K-{1LQPu9N`9dd zphZn3BV;vlpO+;;;U0}KRKuoO9#vo#`g8Q%>{4jPz{0__L$Cp@9n`IX)0(N}0`oB8NvVxQ)2x>qMFpr9mNBk; zTwgCg;dZ#&UGcnPnCA8m`^V{gB=I4L&Cqu#t{fGF2l>!2ZAbA~XmtuwM}0uuVVtLS z_G%R~GT@#xF+D0`Lr>Qy$d8Qk-*z%0lfr=g4rvmJGZw*FXsV zsD>@rc4yVMlPz(?=f%GFGs+=Ec364waMd%v+OOj72M>d^yv5@-eN2ma#FxLelx*jg z;x!}zffM`XpysIx(METa4=ea!I)rCv`ys3zAIGi68(SJ29K3so!kuJdD2eyywxEmy zenFDF@m!^2ww>fFp|&fZ+Sarh&7&mm#d8UcmR)Q#E($gFtOMqtS`JE{inT zY`-yG441(yWe>DNw8gD|E?)*PN*+hNdWyuCBujPeLWl;X&UuQK58KUAUI^wS?S!=EO6YqMUEI>`4TeCGa+{o_U!b z1qqa>j^6Y5+@-J1Gl@SbM%R`G_B;mFZ{!N~|3c?O6AHGy#3H>^BEOFfNf*WME+?mF zCaH`G`i9r}cGwcL-{7Ufjo%>V;@T-EHe0xHydZBGBCy~ZV}|RI&JyK~e^AQeKt{}# z`Vy6>v;FJhpdL;;gUv-M?v0qzyEVOv1-|@Xjd{nSA(Uu#K8J`d1cdwbqLE;2^-n`l9D?8p8Fj|&$k;&z8l2BI=- z;Kd&aiwmAOR1r^p7>r$_Ce7=R5q@3~c}f{r97`M4evKI5@9X6{7yFJqoPa?0Tv@#| zNX7~YS^3$E+j4e&b`_DbA8CGSZn2f%jtenAFL?VFKp19tkKJd;yY7P`Wc7|}QU;Ng z0b;?gB?yD*@O&DzwXE&7)ZGl;Mgo4q9tDu=L5SoHOH-rbp(=J-`>y6!-?M9jujB;EJYCAwrG`UJ_WMi&qNR@XKO^=CJow|xMgbnA7(!Sr~7Gt9s%00S!X(l zrLV8Ck&)5q#Mt327K`;3I-QvLy9T`dPRK7NmbAh0&l8gmnQjZd$Iqi7_Vzu%aT`HUS7u;E4neuKLkA+{F_?+Mo9)|NQy$ui4oQ zrlntLQlee>`+h6TR{Y}&Kp+t{>r}msylAS&)_IM* zZu|v`V{Tsl2vA?)Q=^O|@O5pyqP{DS2LIP{qfP+loBWxO1T>fM@=GhwCEaLdJ9{&M z?QJFiXHH&d{^Q&qDLwcQ00m%Wr68)CAhv)FMFOm-TbyKBe|-G-ak26B1r9DQqX6x* zDnbLnqvmR_G?qTZ3((Q^k>51t%tS~qB!X|Q#M;8b_ulFMX(hTFasWpjhi>Ri!oRXZ zWq?PG`@6!F-#`EREFbXfW|X$G`Cp-&fe56;K*V46PWKB0ri=!KKwg0g5O)0SFR!4W zW0!JT5~e<n#HF?>aFmd7$) zxs!SN76O0tJ3L=sqdrg_{}GA?pP}^@vn>jCa#=wQu|CrLwpL+#%K2Kbzl*~ z#7;}PP|e)fkFUl*X%;Wge|xM3>=~S<_5grY66m?Q6#r5Qc#2Xbg4jU3iaSdZ%0P4b zpC8}7Zpcj;7+;chd#?5$X*OB@YaRc2Uj!M@y#L!{>3;%}-n|ZRFYe%bI#76LNrE2l z*MiT)U+z8vf+uHM`}B_g{up2Ztv0-E+4@dBX5&n+0{t_w9Z%WweabP};QRxUzhzJG zKP!#fe*@r&<`fooyOa4_+)Mi-F~F4iM_v7=k_rB2Rrj}g@m!(!Mj@{Tr}^i#4)_0y z#`)*Me_sMXiK)d<{keZT6|YF}Z2;&{XX%Vj70ASYm}?3CXI1yNdhtB6|52M~udVy= z?}i+4I?`_HSG%sn0<_jUr~9>XBJS%k%~S38apUG$4R7jW@gQ*gkmD~zffKn_H+eQz z{jLBMa9(R`t2aPJvPo0R{TzfF+fe{hU%rvFifXW9R)1R!de!BdWpPbU@sVxRrb7)@~Z z|M2*)EE4np0qi_prJofc@b=Rt4UC<|4;23f?ftLAI3r$)#&?fEBKg&Um&o%}IUWpV zDIMvBcz+(=9RMpS^~y;{dKG{BQBcZT_Ko=*0gUg&3h+Mz_!{BeDl1csT-87X14B7n zsj5LIhXt}shsaIKMgv$1z5a;A=C6Oe#Q10v>q)M&ig?tv?%{ZM&I`P=vy)fs$JSP) z-hA90hU`Naya;~Qw~#pv@!8zENd>EqwcOo2a1B-D$x-e9{zlVt^K+OcY@6=Up{BcG zmB5j?JXMw1VC?pq@S=x~$j^yTV4%Bx*2WrZ$URp3uM5eR5;|mFs{c$q{HyyF334K@ znWU|v>J7-XEIuiDaA(2!!=>JOVqv#7G6E`;0@Qb>#%&#<`KjrI!ZJ$$Jzc$fzj8LP z)iUfQW?task(p&(jmWY>wTisue1?|dBPEdA(xLQ5%X`Y`^aJdn8gl%kD2xHA^!rld zS2O8&I#StFeIanBF<1I|ig5bs1VU5QgizRG&j41y79pead9UQ59e4~Al>x3coMc{8 zmPesD)pc{G1tyM<21J(ihw9nH80rg)RvHTQ5((<)fo}3e*$SR`?g7<5ndLiPgdKR! zVu>paf2lgaTHDzR@+cXrc0E{7h3#=v$P?@K{rvB@>_%&(Tl7lVn)*_}j=-SJ`LXV_ z1sCwCxE}9)n-a1RxxTX4L>(`rFrd=rim6mc*KoGBQR;9V&qqBeut5z9$*5NOL34~k zy>^f{UUX3j7aNwn*d`pHv`5r3_!JOW8j8JHr6arSR`C47IRX#Y zKV4+#Q)6>&0>3W2zEV#%I`~4Iwyx{0N64hxu}z_QyPYz5z)BY;_KwYm>QJQqAF5G2fs+c9~JaJFU z*Y!Y-Q8VpNuGxFEBb)(6D!iO9F9{5Ns5LZAH?!qx0vE?8*IqB-QlF)YTjwW(4*b?- zx0ULDy~zNakICacYpK<15RtBGLx+2nl{v&LDs}>7){5kfUOBgL)8Sl{lZ-B~uk>Cr zY5!#_5x$NbD0Y{8P1)q3oKgJk*?cL-+C;6aS0Oo7VmDD;&Ed=Y)*rS6l06S}*ZXpR z=(A(G3hJ&02|N-bL$ysZdTDL%QX=cTRLEXGAy2GYnF$YPtDtTDNROiW72z&9;Cz+i zK@5dvj=FO-hx__alMy21VuIzIkB82>tg%x4=V;9wK;};|vAH#tRivwx>Y8)}f&^!C z1%Fp_?duE&ys@#ixVy~p5w?IyDJ^ocoaskKmSsi;Wji`#h3Ht6mzmMIiE=xXv_nWZ zqi`IBu#$Dp_62Rpxd%nbD1B7y)DbMaX)=vKo9=>At|5n_(`v_dnzDt>lt|iol)s^y z5w-GH9`QVr?6}&rK+WJ&wAFWyJZ_^eJG7%|;)xjb9rey{q4Lf$lF#Sk3=XjC(HYEv zyyH5oX?MZR!wxIIH)y-A9VvIAO;9$o)qM}&T)JeUXzlV+B3Wl<~{Oc?@-R6*J>v3j5vR>!6oRc9)yJe}6MS&w`%DX2=BD+l)7b z@1}`G26H^%)+&i9#l_oRN>Ol!91eL=Uuo@Bc<}N$ODYvR^Fz>ahyn^{YZ(3fId={_ zDu>WJoqD9kD8<)JvqOv*vm5y?YD2o&Ml{_($6*<~eJ*M}Zq&r^!1^v$oPFpG3%_WL zF2xsxPGfMw)@0vx5DEK>;&&OjzsZ!a$$*p;Yfpx97!kRcWAEQ3P(L|_`jXVZOV;)f z!(NAQo5ei}x}(K!t6CZ41g2t)XVVW|&^)U^PBcK_=AIg&-8+5b2Rq(rzB*N$CB+ZzqK>M;q2}{qQ98eVsrad(H$WP$ zt2`U=Z3idIM$P`VtwYgL2k-_(F0kE53NGem-k8l;^!yGyZL~e*~H!aNum0w;h?UhcF><5+Afd<7s zvn3Nk6!>$Cy_Soy5PCGDm=?aEp7oR@!@9Ls(V51`H%A=Vo zuUpbNAqtwd#XYl&5k)?@jf3@C&@#wxKi|HZ!FqM6oPwuGG|dKNp}=BZxLVLKL2UpT z(CM9!WV-SYl-0n^9)AO+dI5y|a3Y?*G8`MFC^xnj#Z(eW|W>y?G;nCN=fSo-=TeWzjFoE@EwXx?(SbpOw{2VO16w4Kvpj*zo%sDE#W)f?9XG{d(Q=i)_KYQdP z=U>RxkXWi-pP(DClFiE;HF4>UqYC5ZhV>E2Ue9rXDpnZ^3BX4P0^?BFofy@>hLJC(i$F=S+Eg@^; zuOxlvr4!$K4at+Abv7BUsI^X~$!g^nPu-;o@G^&S+xN>Av zvpT8#gniQ{A~3c46A_}=_YS=BetDg{&7ur_ zi;4%8S1i23mKGhTHNqgNC#_;PoYARn0w5(Kui7@wdQnHBG9pD zf?`BQ_m}~unl#R8_dPwg8(&eB9TBLX!Pni8!Oz<;G|Y&j|KkLq-Q-T+tF=N7t;k?a zJJ`UXLpJJ4FZt)_Waz-cB~CTFowiaL&RB=Mk;0koAig5pNc%T{ke_VH_;mJrP-6P< zNoUtA8H*29(`=y^DO=epOpgfC=^`)}dho=P)v~)o&X7$+V#;Xav zk1JB~!+^_TB>({_ocoklxjq<~Bw}tX>uSBgwD$NQdHtBZI_#-x6%3?}o?Lw%2sXL< zLa)0i;U{^wmu4Bd=v|aafXU~;3O70+kfL^~$hW*5bf z;4Zh@v2yHE%*Ki@os9d`&LyHJw?Ho!c7$7ojY^o5Um2c3>^^uSD|u6LVEfBN@TjQt}r=F zR4o0LUWk>81Ml(MfbGfJXhHcG%2Y-;7s;|x?y`L$g5O$q|BN6)!JV91U#DW28YeMHM*wm1N_^gB0yl+1P0Q`j}z{^^BEH8kj!+J4~HIpmmOtZVIP{Kt-b zO-n)HwELfMDYw{}lHcanEUQ$tmoZP404sh!(DfzVk;z$mAVNv$L23@~&MaoO2O!(6?1jnJb|e zD-z=ay=;#=T-*CSTMWhVXNLQtatbs$8h*5ge#&|5q&fbsmY=>s!WqgNC7!8Nx=Yw& z4N;acg8eMw8OoAOZ?UABC5vG`+>D|awd%9f(sR%4tl_LNH69={M%KFPFv55BuU<{~ ze7O^jOqId zMEc_VIX-v0Md9fmZxyv@YdORU>S*Pw&agL)Zas&Uzi@B$t!s{V7>|pOqdjLbaa3M6 znH_miLM<}S9e^gLllLo1%uLEc`KJ;4<_C#u{UIM7b?1#e&b;|v@oMx@(DCg40K(dV zBC%itqB8pa9dFky^PC*+=+@{%{+!4@>$R~Yde!8ic8`O^gG^pZh4_2Blt64gh^)So zHPmI$v4#=M!KDPVRm-Vo@MW`E*N9*equ;T%w^w%FGP)?RoT%ek@k%c9YpG&XQ+sGP zIR|HqH&|$9V*h;?MsGJiC^x&cMKsn*qoOk;lNEfJu;VF^ zZp3~;>HKxv*A1em-fG>AlS;jKBa^i2pjj?kf|2%rR(j-zQ`E>cr9>+qdqb4_i8KQ( zh{eWwT4gjlyeJ>D7yZ%4b`;KcYxBV)n~;ncXaoEll4`t&5%D2)cl8Gy$v!W~d|r*W z>xpeLReffPeO8uOWxbyy!}`Onn#1=6wc&Eq1Ae~lT$zsx-@IOR?%+|hwv`zp4_PIo zUp|ntR5x=aUuq``5||K(a8^&?vark~8uF=v+1OgW=o~+O;av|HPit z5_lO+_o~|5AylAfKe+}MnF7_qS_im&%0e-mt2T^pNfRQckmpbvrRa!r;tN~J7I`6@Z0Kj|t2pdH z!&H@5Hk^O?V>r0>*22zawt;@3$i0~>x*lI&n^?N=8y9#Rhtp?PRXxjWNz}Ae=H$3V ztA?8i$Q%C@6XpSxO{$k-_Y8`vvO(9ztXv?_<@u+y2_VwcyRQ^BF;M(e z>BE{VX^xzFq-#~>GFV<%!dYean8EK?@`dpVPfPjhL8al!z^G4ncv-wU7lhe&*_Q|v z>^*S7>=|W}1gnA3X>!v+i~2ZMa_K|N9H;5gV9WxVmvYq%e{Qj z!nztP8v(UY-U{kwV7gL9N}DsdD6zcr=wPS8rqx~5D_?;h>L8KcJ0Li&U)Xuy`Z>mJ z2@H8N*U{~#6TXp3I;aV8bJDFM;+1_7;~`{@TuBu3%DYEh4e;!?F+p^V?{NDu^lI^ycZcL zXKH+y-&^F+?UM)@*^VYn9Xrn@0Z~V1#8|baNSLsT!*0S98a$HG941B$PY@h?_B3>& zzOdlaoyx&Q;?n+Z0Xbvir&DVBYS-LAPZg_EHb&a zMd}u5yG&fpH+M-^=FJ33b=vDN{+)zH+$-486Th{{x2k9_VO_z3T?K(BPC9zE#^bww z?stZr0Sqq+^^cjre8e1K#TsTk&Z}_8)(@lcq`*8Z&b?2)rymp_e*T3$rr5%I6i6X6*;d*f5V+o#l4?)m9==EIvn#Txv ztQ+rK|JI4_aJN2&JvlFoAJDR0F@&)kVf>4mp{9gK+j8{H@XMgAVly|)QOy1`bnB##w!*7S!(cjF=O4hYJ>`Dciny;R*CZL8Z z3!n7LjPKRKPz<0b!NNvMI=d6O@vh*|Dvo?LshvZ45--YpkCJ9`2l=(}j|2Ri1`kGT zL6C^w?V`GY$1emL8f2f;7#ncd>@IE4kYvS=WiYm>J3T6BF3 z3841K-<(4|?lglQ862*!)B=}%F<$iz3u_P7LPtvN!V?-Ea}%=uR+@3DJ~jFRB})t# zX*{^DZzA1M;3)Y1bV!BhHG%@0BZJEXjrq*HS+^TBEOeA;f~4C(d){AO+#U@-VBU zgdNyP|8Y2JXk56w4B^UMu}WUv%2Cl6;On37=HQD1)K}-I$x>DdbPlcbQn()*^)qi0 zZiFCL2(j$#y~r;vnDwDU;;8%N64=r?lcTWfTezhazo1}NZU=k!g9k6uid6$MEw$ow z@@wvkG5t8bsw#<%%-Vl7?697%vSyheX$Ig{31 zTczF@%aRqdc))9if*X<3{61-!hT~4WE}cKOHc}$B(SY-XCtffO-Gm9?*Gr}sMl$l~ zYPBj}V7fM}%k)It;wzx!x3qt~>nC0tF^cqRc@QiZm)BVnAV*xShNLKkT+eGa9!U1iDj% z8XdL%=BuC(GIfA=lE545Pg5f&C27#G3s%NDd|=6N=s-f8{H@N)hpy?`4R|z@&sGbi zjprQ#-QoSJ8wCD33-jHH(4+m5N_%8pulV5uRQEZ8-=Ukw7!>D?%P#>pR4DEkvjaG* z*u4Fh^(XtQw-5kTU@D3HV*+oK!JEbZqzE#=82Ot#)eXoiOu69Q_nn1aD!R+S)q?x~ z3KjX~ADft2iSF+H){C{V3I%}00&5Rga2(zO-ND+!L-*6CPgr|me}NzWe*IsQ-Tze+ z-~YS!Yy35RIsu-b*Wa4+Cf+8-|8F4}zm4Poyyf0KKWis)QIbvo+BY*(+Ab-8cuseG zV_EfuLcTt=!Qeu7#h(6VUOEsKfGc!)afkc4Px#o%ik1nA#oe@$BRbT>)d+=_K1^I-yw@zH2O{%~gH+#WOF z!^CwK3e*1@%N(g}g2_{n*r7Ku56g!$Rg{wlmj+npuDty?+^bL^z%*Q~;&L!+on-62 zKznX`8B?{VNKv~SMX9?6d1%O&0k4$G?Rbm#E&dJnJx1%0pc>M;tfDGc#d14ilS%rB z@-3Ep0M`fYKL&q~!UB{~+02J)E6e4tDlk>^KeHCu-UyEtMk!{!jIv5806Pp48PW;w zp#$~-bl^Mkb!(lOd$k4H`L=FB`eRa|O=TTt>~xV`=9sn{NgSwHI;e#>sb$d zxLG>T_N{hYOLa3A0@+(B9>{Gzw9&+Be^?D&qu!;V`<1_5DMKFfta7vgw5_qDoGDLC zJUFzZU#E00eEBLd;OiE>C7q3F$b9qWmeF>GJ=JFIV4p7FQfyQ1hZD&wC{R;U$|NSH zp3|3ky%gc6YKQJSrWG$}4oY$ozMh{|`fRV#zD0RpKo9ojO_fWjP)#Z>W0HeSwIi)3 zBcrju7c_h#isS+SDQe)S>7s<$GfTFbVDJZG;;|URevjOR=-*nk^%9o=koLSw zsd=n_A=tg1e&Dcyd>y{;gd3{DSbSM1J?I`Nd=qu+Q_St0f#1__MU(n$bW-Wl7nrhz zUd8~99i9?rTAcT@^AJ`0iL0ECo!gp&xXu1rz+{=Qp|ws5$Te|2%YE?3)xo<1UK2w# zjYfmBsYQ?>k<%H4)Yk)3Mn26|{HW=AIc{Nm1vI!FAG3w8>1tD9Td>U2^`;RcJ zE-&`=m_}%Pvj8N@IV6#=wjcWzcSV&-cZn{vSqN|`kFABN0K&izje-NM4r?jd#cfOT zS?|O+{5JFMYdM$lrYTi_m2fHC$7cRSoq9~#F(*T7#nKvBKY(um7E=yXA7+%oc zABEL;->)efMz3CZLaCpSKsZa1vF^+~)ZMZqkFjvp&%f z^RQB@Fs`95en%Kh4m{r>GRS1*<_SNx3ASA1^_UPqb};l4l(86 z;8bT#91jobSD7hLimDglRDQ&nEYT`n+|ucH$T#ym)jmXLhWW-}ttCEw?^V*!r)EmZt&Hd<17hg#9onNl!gf{&l_){oXqIQywEUmqHQ|5zz8nQWxex}a5&D%JDQfO zP@dl8RZgx}P-#JZ{|#@`VvUlmM~6KVstdUpjGt{GYlm9*4n>3y0dXs(lC^_utwTY5 z0tI1N`*dHWxZ+$&4uh333fr;}?a$@?z6K;+D|k&Q{KvUdW^Zi(F70~_YqW*kSm91q zkY`~UOo4>p*7u&Zv5h3`{#=OO808blQi^9F{)QN!^C9xSa&tcOV%iFKWqU0AmzlEqd~!L@8)COu2c$T!5o9aSv!)SnYch`oVl>8=Qrs!Du-uC^l(j@_T+kN;{! z+SBF$F)9Z;)*Y9soGh$-XQ^ucmyP3WOnwRM-iumQhp)>1T`M{UF0tMKs0%ipsXUx& z$Q?IPfBepQ1V$Lto%#zlS}@X|`PSDCot-gaE>v$dw;EbU)&QGQNP^_WHBUv@Q91Y4 zb?yk2jxFqL)3-BRw#M#Y-p;Ovs7Nw-#U}J+09<{c3*iKv4!4=Oz-y9h$6Sa#-h~Cn~5%>X`_yj6tm;I&YSBmaYD&~zc z#`wMW%WrHZz3dhrtXb=(cZA9l7Rux$(~TT<+T8R(Hl&>J6--UFtGK)9Qw>+onnIJ{OEp=C zm>_iiM*N$p;&-*m=3+ha1w+@KvgAwA`8>arf_1^QhSzwUC}b)UBE0m`Yoj42t#!*g zy_OX{3l_R-=q;2=%1W0{p(MSl@!~MRC^G2BEUcwvo346yW$(4>-7X>hJ(ALd<5pcj z*A5At`3V#girx#c6|7y|D`!a5^KCHkZ2;+;kK`xo(pe?-Iet?qR6>`I+m-77zsMpwY$K64q^Md)L;D6izgOGlHos( z3n#I4w9(x~9tRm!dSb~f;qyyN@0A{B_$;jPb}<>eD5sXeF;|z(l1w1u%nUkezU!0p z%O7yP5>Hc|ul5uRpaL9<8xNkIsY`9J)5C9=S!Pd?A*wn}9>Adv_*H=bu^VIk1EX7$4QPeL8vT;kp!kmD3JDG<#e++gWJLE5QSx z455DjP*S3Q08kX|^*PW)Ft45qTV*Ez`uUbVFvP~xYg)C;nY1PJ3n9X`ai0&7vEoL; zaid*)t-CYcBD8H4q-IbBfMcCK4dVuBZOXg9#1k#9MR(Dt-o2rpw4*Ya!W8}F<~34~DSl~L(whqmOmAZA&on@mvl z$?b(Lnd2=M6W3?9JDMXc^vp+X9~`Z$czl~d`h5VNs1k(L@bna7Qrde~LJK(?H;j!w zLMo`MPJ~8s*8}RHORWKg8-7Oz=07dFzqu@_^Oue4>Q0FF9sicuQuG&1XesoV#^{Qw zhc1tTzKZOgV7waM*S+E>ayahUDDf~CEVUIRO{t?j6l3w)JpR$s@qUbL;py_0^NRH@ z=*e^&?m=rLM8ML@DqUafRvgSuh2={vmhI%IXcV(7bF{gbU6|rKI?>y&&R4qJ!S;@9 zI)8lYIP+;MnRwl&!QUwcVsK;;^DEaaIdeS@`8=JLu8&4MzWDbI+cDk7!hzNHQ z3-UA$fu>i^=v_!^XRJZyXp%a9Zx|0%uA=cwmW9`{ebf{(h80TJ4va#>Br=EITfV?? zqH2?rT^PgbN_Uu!iVsm6$M^3GC%Ki6C%tb^SI?K_!uFBCQ?^!QyzID)-Gkex7u52H z(<3JpZy$Oh*JbFDi!#jA-Sg+fgPMjP|7-|wtEEG}&(Vaa(HwvES6S?cW-Y0yq0UxG z2?L@lfO&}SM8|`9fT}jtpZlcGuR?e9%*8lY+PQ&scK#EF(wc#vbk20)( z^P&84z>$na_yWg|%~RPXQ*w$l2|t_**swa<&Jjp>WY|A4(MzOKOJ6Z{LN|E*RvUqT z?>htfvuzgeACAHZrax}p|94LCMdNM!jgRZRvAo6rhHlA%K!Ux_3uE7}Kp=h&j{^BD zUvJDa3HK3MvhY@e)*C;2X09hV^$FNIcybGH;PlURS{*5Q4BcJKh})8`@x%E(d-et7 z=lh-W+;0U}AUGPB=iD8}(-(2*#kTzXh+Dn}@$nHGTXTE%tj=?XWqx6SweCIvfw&Mo z08Al}5cS%#?cF#>ka`!Nt(b{}18BK^VPT3vp>_cbrs60~K;RvA5!mVRj~chR--L%& z)Ly~&FXl=w;WSeK6XK6Ny?pC%gy{`CaO1(*EfLOFTKvhlV{Z*(JQ7owE3^eKt!^KKnQu16~-@;85qNMxazAs!ubb0Wlo7 za!CUypt<|_0#(dadGz#DI?+?<*!kC&-r~!_fBObroGew`tORVBrfb-^?L2`g^z56K z7#GP{ix*f9d@k97&RV&D9}ulEGL5J@t1O#^; zk)0h#s1L26q0z>tJU|4So_uMi*9n+T>m6v(QK|nQJaNom*~p#7IAA}A&m34<`B{;q H>6`xz0NRG~ literal 0 HcmV?d00001 diff --git a/Gigya.Microdot.ServiceDiscovery/Rewrite/_diagram_from_draw.io.xml b/Gigya.Microdot.ServiceDiscovery/Rewrite/_diagram_from_draw.io.xml new file mode 100644 index 00000000..8de5324b --- /dev/null +++ b/Gigya.Microdot.ServiceDiscovery/Rewrite/_diagram_from_draw.io.xml @@ -0,0 +1 @@ +7V1bc6O4Ev41rso+HBdCXB9jz8zOVGVO7Ul2a58VW7GpwcgHcBLvr19xEUYStxBheyZypVJGFhLQX3/d6pbEDC53r7/HaL/9TtY4nJnG+nUGP81M0/cg/Z8VHIsCmxVs4mBdFIFTwUPwDy4LjbL0EKxxwlVMCQnTYM8XrkgU4VXKlaE4Ji98tScS8r3u0QZLBQ8rFMqlfwfrdFuUeqZ7Kv+Kg82W9Qwcv/jlEa1+bGJyiMr+ZiZ8yj/FzzvE2ipvNNmiNXmpFcHPM7iMCUmLb7vXJQ6zR8seW3Hel5Zfq+uOcZQOOcGxizOeUXjA7JLzC0uP7GHgaH2bPVN69BiS1Y8ZXKxRssVZE4Ae0N+/BLRV+Mkojkp5Ano3i226C8t6SRqTH3hJQhLnDUMj/1S/sCed1X0iUcqascrj2pl+/sl6W1O5lZdK4nRLNiRC4edT6SIXRn6tWU/FzWVncQ8sIYd4VRY5lvwQGQZRvMFlkQ0rcVEtwGSH0/hI67yc8AKcUsoxDlEaPPN9ohKjm+rcqrk/SEB7No1SnWyGrlKbfNfimyguvzyrLm2hIc8VGjI9vqHiBqWGqPjRsVZtn1VI2i/YBd7c5nrySgIoG6RfijbZUe0RnopyrLYAHUi4/fZfykIP+ZP4glYpyaVxkx73+DcJ0slLsAtRhBnSyl8y5KEw2ET0+4oKH1O4LZ5xnAaUGm7LH1Kyp6WrbRCu79CRHDI8JCnVe3a02JI4+Ic2i07IRzGDs+lwNR6yM0twxjihdf5guANC0Xf0ylW8Q0laFqxIGKJ9EjxWt7GjkgyiBUlTsisrsZvOlLWmSyU/iZpSMVN2EKJHHC4qbmNnRyR/hJJiV+rZoNi1zsvTh+p6KXEqDvzaSndMU2OM0tvkIYg2TObGrFFfGWR9AbEOLJW3ptCmVZZta+QvKmODPt9TG4XyK6n6q5o6ClzRxB/17ih+uO5QSGEaoRQvMskkgpa9Wa9MSa+oDOlTpGV/UlXKVArPN/NZ9jid/x8yO0XRFyWH8HRM9Y1eJAQnwq2pHhVeKmGGIUEGB1PHED+lrcqY7NGKXuNdXueTdSq5L59bVkTouU9hDultsF7jKId8ilJUaE12tSWx0Qu1F/SP3uPSoLiw6YUv6TE4HdO/rHpMkRrRe0FBDkBMVfIFZ2o5EK2gGZRHXtZ9EDQ7IMiBoUPyvtPFqJn44+My0yp8k+D4OVjh22j9OXrWwlYkbHsg36gQNmv3Im5fl3U4r9vXYDjqnqDvyW5fmxAncPsA659ZJNce5/Y5fDuWN8zrG2E+LLcfVzVoiAr+WLoriwbsTQIjHrSXQ5Xf4pzUUOVNAyrXsOdG7QPGQUxodOC4YgTCbNhjpiTrpF1/7fo3axcbXtu8K/4xPX/bkhTrkZCsxt8o+YuiYx+SI4WI9vaGobEtPnQJ114Wbc6Z+ZOgR8bvOM2OkxvtzCsS7zmdeUc78wPcLrvB7TpjDNf0+NCr6wou0lBPS2rIMgf5WgpiuIbaGK4j0xKlnzAtSCCi3zfZd6qmT8Gm7uMVdR5jVoOV0A7rJ0pKUMPxtTs8kisq+qrNdDrcT2r1hiwoRkI9ln7oIzPmN72HzJia9oDijtB715g4FyYEl9X1zogIVw5MdknxQ8YUWDrsjcZNUUwBSqbCNU0VUQXoSDZIRJS6yII7IHb1wVLhwOgYZl8oF+4LKWw2s+TNESvLn8PaR8j++fYgnL3fqwKGrdatYjl+jeMKx4znrgfGXgMOzJFIdlxv7rUimfZ0JiT7DlAKZK9pXNs4PkgO4f8OOD5qh/BigwTfOqNL6Mmzf7RL2Ga7L5NnsiCfEhptp6WElRg4UecBeqaGVR+sGDwuhCqLB4PnwzlwTh/3+jHWkL+8I2hNyRxFK8ripnFTS2jq/KXOX/blLy1oCMbYakg/QFNZBtMGovWv9PuyOUxPHsLzkwM4B1Enuoag0ntzHrMRaUrymPJihW/fSRSkJMbrPKFZpTJ1JlOVgIdSiRIByyO+YnS3DANcml5tC7UtbLeFHp+o8OHAUandMSr96aby+PLwuFCje0z/pSwS8ic93NNyfIqFGEsUhjeHONT0ORSpbYOfS8zzkXOnhdwbVkZpJtVM2smkwlJB1/6QTCrnUr5i2sU21x6UHmivt7OlObutLYdaCsuhNJUOpVLvaqgUGAMm1X34WGAZy7rMrAPH5oN2tm/N7ffPOfDdNzWrKJvmuDz/sTnp7Tm/zvrvX19ttPmQ9RCKdiK0E9HpRMh5QmAMXV3xSzkSwJBzS9qTmG6prXE9C6uB0ZDy+RQkq+zBHfWATHPpQC7liW3iHM/V8mjDWiYhf9qyUUFGNZnTiVZb9BiEQXrUQa7hfHo9WSBgNKSB9E4V5xT3OXNCwJDn//MKry2ntpydltM2xKmqcOj6zF9sECLPj+gYhBQzbQsGZSmi/7BynBnUamatptVhtOpc0ajkkssEfppVw5X1qcc422ZMTLEHkCPOTBy5B5DU0ETLhh1hKxjo9gQyu+sr2ChSDuTLg295VYFeQDDtAgLeQMIGAznZ6gHQsHmopj6Z+pjmcNuftY0MJuA+H/DUAAxxKdPgnI7Qku9OQ37VnbMYDegmv576CshPjjp38dWHzGI27vLXFj9Wn8Y0DXHvNCUbsknN2tMsA4TS0m9+p+f+Eygde51nWG7fKQoURY7Ra0WRfOGGfP8AX1hVvp/N5WRBKDh2vazQkMumOqlf7QNA0x43Glf9uDrfPBJLjg6ZIwdZtt3blCLaFR1ou2+YBTvrKyDQAS+S0EBvAPqADYVVeRqS6bXEfVoHb8zi9TalyqX2pZ68bneh9wwFYG/YK11YiCRAXycldFJCSEoAcdUmGLqFgi+o2s+dlAByUuIeZ7H2+yJVH+rFfG95x8AVpRiAvB6lkOxfUVzJFr+u8D4NSKSFrErI59x8FgA5j5TvG51nEb8llRJr6SqSLpvPdRbpmnL25Jt2cLSDM8zB4fwNCIbh9tear2jKuabqhVpfSZJGaKepcTAU2yB3Ce+GBThqks3v6kt20fROtVQVSfWs7ow5IGCrc8NVBISLZLXJcYrt9OWg7djkcENT070gC5hynPS+NiV9ucUUT3o+yrnno1SObrWh1UAvW8mEFFOOJ3aJ8UMGz5sWG5fadJbso+vNTQtA16cmHVoWn0K0HDD3659xVNTdiWNME2K3hFko0OrOJfXUf3943dRvERyZSzqfPlieP/cd2wZuCVjBgo7UgO5mKwZWPYlFYP++eVs99RVoQNNbULQG8Bpg+lesARVJXkYDRoFOjuQKSU3tll7YLQXMCzqLW8oa0WPht/NQWxB/grEwNKXN+EdaX5u2VPdivc5WFVIPlKOkmnGugnHssxKOHFLVhCMTDtOWywXf/Hk2odwxDcfLfBWeJ0xjpOvT0y6wpgvLwXFh3w8AtkbrBvsnLSqzbobgArkj0WUbxtxqNW5SsyrBpcN7/a/nbQxn9HOaqvCeZ8zzkJtXEBAQjKA1DnPdzToThTNcIfPeNznc9d9X31E8vRbq+N+4cPiAQYeq4Ifjz+uWmgf26OAHFLa1hIY1L2PhhQZNoi+WL/bat5gCdNV/qwLQw5hk+/udqsdov/1OB0BZjX8B \ No newline at end of file diff --git a/Gigya.Microdot.ServiceDiscovery/ServiceDiscovery.cs b/Gigya.Microdot.ServiceDiscovery/ServiceDiscovery.cs index 75898e22..335f84da 100644 --- a/Gigya.Microdot.ServiceDiscovery/ServiceDiscovery.cs +++ b/Gigya.Microdot.ServiceDiscovery/ServiceDiscovery.cs @@ -27,10 +27,11 @@ using System.Threading.Tasks; using System.Threading.Tasks.Dataflow; using Gigya.Microdot.Interfaces.Configuration; -using Gigya.Microdot.Interfaces.HttpService; using Gigya.Microdot.Interfaces.Logging; +using Gigya.Microdot.Interfaces.SystemWrappers; using Gigya.Microdot.ServiceDiscovery.Config; using Gigya.Microdot.ServiceDiscovery.HostManagement; +using Gigya.Microdot.SharedLogic.HttpService; namespace Gigya.Microdot.ServiceDiscovery { @@ -44,13 +45,13 @@ public sealed class ServiceDiscovery : IServiceDiscovery, IDisposable private RemoteHostPool MasterEnvironmentPool { get; set; } private List _masterEnvironmentLinks = new List(); - private readonly ServiceDeployment _masterDeployment; + private readonly DeploymentIdentifier _masterDeployment; private RemoteHostPool OriginatingEnvironmentPool { get; set; } private ILog Log { get; } private List _originatingEnvironmentLinks = new List(); - private readonly ServiceDeployment _originatingDeployment; + private readonly DeploymentIdentifier _originatingDeployment; private const string MASTER_ENVIRONMENT = "prod"; private readonly string _serviceName; @@ -70,15 +71,15 @@ public ServiceDiscovery(string serviceName, ReachabilityChecker reachabilityChecker, IRemoteHostPoolFactory remoteHostPoolFactory, IDiscoverySourceLoader serviceDiscoveryLoader, - IEnvironmentVariableProvider environmentVariableProvider, + IEnvironment environment, ISourceBlock configListener, Func discoveryConfigFactory, ILog log) { Log = log; _serviceName = serviceName; - _originatingDeployment = new ServiceDeployment(serviceName, environmentVariableProvider.DeploymentEnvironment); - _masterDeployment = new ServiceDeployment(serviceName, MASTER_ENVIRONMENT); + _originatingDeployment = new DeploymentIdentifier(serviceName, environment.DeploymentEnvironment, environment); + _masterDeployment = new DeploymentIdentifier(serviceName, MASTER_ENVIRONMENT, environment); _reachabilityChecker = reachabilityChecker; _remoteHostPoolFactory = remoteHostPoolFactory; @@ -175,11 +176,11 @@ private void RemoveMasterPool() } private RemoteHostPool CreatePool( - ServiceDeployment serviceDeployment, + DeploymentIdentifier deploymentIdentifier, List blockLinks, IServiceDiscoverySource discoverySource) { - var result = _remoteHostPoolFactory.Create(serviceDeployment, discoverySource, _reachabilityChecker); + var result = _remoteHostPoolFactory.Create(deploymentIdentifier, discoverySource, _reachabilityChecker); var dispose = result.EndPointsChanged.LinkTo(new ActionBlock(m => { @@ -205,9 +206,9 @@ private RemoteHostPool CreatePool( } - private async Task GetDiscoverySource(ServiceDeployment serviceDeployment, ServiceDiscoveryConfig config) + private async Task GetDiscoverySource(DeploymentIdentifier deploymentIdentifier, ServiceDiscoveryConfig config) { - var source = _serviceDiscoveryLoader.GetDiscoverySource(serviceDeployment, config); + var source = _serviceDiscoveryLoader.GetDiscoverySource(deploymentIdentifier, config); await source.Init().ConfigureAwait(false); @@ -227,7 +228,7 @@ private RemoteHostPool GetRelevantPool() if (newActivePool != _activePool) { - Log.Info(x=>x("Discovery host pool has changed", unencryptedTags: new {serviceName = _serviceName, previousPool = _activePool?.ServiceDeployment?.ToString(), newPool = newActivePool.ServiceDeployment.ToString()})); + Log.Info(x=>x("Discovery host pool has changed", unencryptedTags: new {serviceName = _serviceName, previousPool = _activePool?.DeploymentIdentifier?.ToString(), newPool = newActivePool.DeploymentIdentifier.ToString()})); _activePool = newActivePool; FireEndPointChange(); diff --git a/Gigya.Microdot.ServiceDiscovery/paket.references b/Gigya.Microdot.ServiceDiscovery/paket.references index ddba4425..6b32ada1 100644 --- a/Gigya.Microdot.ServiceDiscovery/paket.references +++ b/Gigya.Microdot.ServiceDiscovery/paket.references @@ -3,4 +3,5 @@ Nito.AsyncEx Newtonsoft.Json Metrics.NET System.Collections.Immutable -System.Threading.Tasks.Dataflow \ No newline at end of file +System.Threading.Tasks.Dataflow +System.ValueTuple diff --git a/Gigya.Microdot.ServiceProxy/Caching/AsyncMemoizer.cs b/Gigya.Microdot.ServiceProxy/Caching/AsyncMemoizer.cs index b49cf532..f0b84049 100644 --- a/Gigya.Microdot.ServiceProxy/Caching/AsyncMemoizer.cs +++ b/Gigya.Microdot.ServiceProxy/Caching/AsyncMemoizer.cs @@ -25,7 +25,8 @@ using System.Reflection; using System.Security.Cryptography; using System.Threading.Tasks; -using Gigya.Microdot.Interfaces.HttpService; +using Gigya.Microdot.SharedLogic.HttpService; +using Gigya.Microdot.SharedLogic.Utils; using Metrics; using Newtonsoft.Json; @@ -62,7 +63,7 @@ public object Memoize(object dataSource, MethodInfo method, object[] args, Cache if (taskResultType == null) throw new ArgumentException("The specified method doesn't return Task and therefore cannot be memoized", nameof(method)); - var target = new InvocationTarget(method); + var target = new InvocationTarget(method, method.GetParameters()); string cacheKey = $"{target}#{GetArgumentHash(args)}"; return Cache.GetOrAdd(cacheKey, () => (Task)method.Invoke(dataSource, args), taskResultType, policy, target.MethodName, string.Join(",", args), new []{target.TypeName, target.MethodName}); @@ -81,5 +82,11 @@ private string GetArgumentHash(object[] args) return Convert.ToBase64String(sha.ComputeHash(stream)); } } + + public void Dispose() + { + Cache.TryDispose(); + Metrics.TryDispose(); + } } } \ No newline at end of file diff --git a/Gigya.Microdot.ServiceProxy/Caching/IMemoizer.cs b/Gigya.Microdot.ServiceProxy/Caching/IMemoizer.cs index e9b03bfc..3f5737ff 100644 --- a/Gigya.Microdot.ServiceProxy/Caching/IMemoizer.cs +++ b/Gigya.Microdot.ServiceProxy/Caching/IMemoizer.cs @@ -27,7 +27,7 @@ namespace Gigya.Microdot.ServiceProxy.Caching { - public interface IMemoizer + public interface IMemoizer : IDisposable { object Memoize(object dataSource, MethodInfo method, object[] args, CacheItemPolicyEx policy); } diff --git a/Gigya.Microdot.ServiceProxy/Gigya.Microdot.ServiceProxy.csproj b/Gigya.Microdot.ServiceProxy/Gigya.Microdot.ServiceProxy.csproj index e17cad56..587f230a 100644 --- a/Gigya.Microdot.ServiceProxy/Gigya.Microdot.ServiceProxy.csproj +++ b/Gigya.Microdot.ServiceProxy/Gigya.Microdot.ServiceProxy.csproj @@ -63,6 +63,10 @@ + + + + diff --git a/Gigya.Microdot.ServiceProxy/IServiceProxyProvider.cs b/Gigya.Microdot.ServiceProxy/IServiceProxyProvider.cs index 19988ad5..84ca2820 100644 --- a/Gigya.Microdot.ServiceProxy/IServiceProxyProvider.cs +++ b/Gigya.Microdot.ServiceProxy/IServiceProxyProvider.cs @@ -24,7 +24,7 @@ using System.Threading.Tasks; using System.Threading.Tasks.Dataflow; using Gigya.Common.Contracts.HttpService; -using Gigya.Microdot.Interfaces.HttpService; +using Gigya.Microdot.SharedLogic.HttpService; using Newtonsoft.Json; namespace Gigya.Microdot.ServiceProxy diff --git a/Gigya.Microdot.ServiceProxy/Rewrite/IMemoizer.cs b/Gigya.Microdot.ServiceProxy/Rewrite/IMemoizer.cs new file mode 100644 index 00000000..1a354426 --- /dev/null +++ b/Gigya.Microdot.ServiceProxy/Rewrite/IMemoizer.cs @@ -0,0 +1,35 @@ +#region Copyright +// Copyright 2017 Gigya Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +#endregion + +using System; +using System.Reflection; +using System.Threading.Tasks; +using Gigya.Microdot.ServiceProxy.Caching; + +namespace Gigya.Microdot.ServiceProxy.Rewrite +{ + interface IMemoizer : IProxyable, IDisposable + { + object Memoize(object dataSource, MethodInfo method, object[] args, CacheItemPolicyEx policy); + object GetOrAdd(string key, Func factory, CacheItemPolicyEx policy); + } +} diff --git a/Gigya.Microdot.ServiceProxy/Rewrite/IProxyable.cs b/Gigya.Microdot.ServiceProxy/Rewrite/IProxyable.cs new file mode 100644 index 00000000..c2c9d669 --- /dev/null +++ b/Gigya.Microdot.ServiceProxy/Rewrite/IProxyable.cs @@ -0,0 +1,42 @@ +#region Copyright +// Copyright 2017 Gigya Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +#endregion + +using System.Reflection; +using System.Reflection.DispatchProxy; + +namespace Gigya.Microdot.ServiceProxy.Rewrite +{ + public interface IProxyable + { + object Invoke(MethodInfo targetMethod, object[] args); + } + + public static class ProxyableExtentions + { + public static TInterface ToProxy(IProxyable proxyable) + { + var proxy = DispatchProxy.Create(); + ((DelegatingDispatchProxy)(object)proxy).InvokeDelegate = proxyable.Invoke; + return proxy; + } + } +} \ No newline at end of file diff --git a/Gigya.Microdot.ServiceProxy/Rewrite/IServiceProxyProvider.cs b/Gigya.Microdot.ServiceProxy/Rewrite/IServiceProxyProvider.cs new file mode 100644 index 00000000..f1d676a9 --- /dev/null +++ b/Gigya.Microdot.ServiceProxy/Rewrite/IServiceProxyProvider.cs @@ -0,0 +1,58 @@ +#region Copyright +// Copyright 2017 Gigya Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +#endregion + +using System; +using System.Threading.Tasks; +using Gigya.Common.Contracts.HttpService; +using Gigya.Microdot.ServiceDiscovery.HostManagement; +using Gigya.Microdot.ServiceDiscovery.Rewrite; +using Gigya.Microdot.SharedLogic.HttpService; +using Gigya.Microdot.SharedLogic.Utils; +using Newtonsoft.Json; + +namespace Gigya.Microdot.ServiceProxy.Rewrite +{ + /// + /// This is a beta version. Please do not use it until it's ready + /// + public interface IServiceProxyProvider : IProxyable + { + Task Invoke(HttpServiceRequest request, Type resultReturnType, JsonSerializerSettings jsonSettings = null); + Task GetSchema(); + HttpServiceAttribute HttpSettings { get; } + } + + public class DeployedService : IDisposable + { + internal IMemoizer Memoizer { get; } + internal ServiceSchema Schema { get; set; } + internal ILoadBalancer LoadBalancer { get; } + + + + public void Dispose() + { + Memoizer.TryDispose(); + LoadBalancer.TryDispose(); + } + } +} \ No newline at end of file diff --git a/Gigya.Microdot.ServiceProxy/Rewrite/ServiceProxyProvider.cs b/Gigya.Microdot.ServiceProxy/Rewrite/ServiceProxyProvider.cs new file mode 100644 index 00000000..378f62fc --- /dev/null +++ b/Gigya.Microdot.ServiceProxy/Rewrite/ServiceProxyProvider.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Gigya.Common.Contracts.HttpService; +using Gigya.Microdot.SharedLogic; +using Gigya.Microdot.SharedLogic.Events; +using Gigya.Microdot.SharedLogic.HttpService; +using Gigya.Microdot.SharedLogic.Rewrite; +using Newtonsoft.Json; + +namespace Gigya.Microdot.ServiceProxy.Rewrite +{ + /// + /// This is a beta version. Please do not use it until it's ready + /// + public class ServiceProxyProvider : IServiceProxyProvider + { + public static readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings + { + TypeNameHandling = TypeNameHandling.Auto, + NullValueHandling = NullValueHandling.Ignore, + Formatting = Formatting.Indented, + DateParseHandling = DateParseHandling.None + }; + + public HttpServiceAttribute HttpSettings { get; } + + /// + /// Gets the name of the remote service from the interface name. + /// + public string ServiceName { get; } + + private ConcurrentDictionary Deployments { get; set; } + + public ServiceProxyProvider(string serviceName) + { + ServiceName = serviceName; + } + + public object Invoke(MethodInfo targetMethod, object[] args) + { + // TODO: Add caching to this step to prevent using reflection every call + var resultReturnType = targetMethod.ReturnType.GetGenericArguments().SingleOrDefault() ?? typeof(object); + var request = new HttpServiceRequest(targetMethod, args); + + return TaskConverter.ToStronglyTypedTask(Invoke(request, resultReturnType), resultReturnType); + } + + public async Task Invoke(HttpServiceRequest request, Type resultReturnType, JsonSerializerSettings jsonSettings = null) + { + if (request == null) + throw new ArgumentNullException(nameof(request)); + if (resultReturnType == null) + throw new ArgumentNullException(nameof(resultReturnType)); + + var hostOverride = TracingContext.GetHostOverride(ServiceName); + + if (hostOverride != null) + { + var httpRequest = CreateHttpRequest(request, jsonSettings, hostOverride); + } + else + { + + } + + return null; + } + + private HttpRequestMessage CreateHttpRequest(HttpServiceRequest request, JsonSerializerSettings jsonSettings, HostOverride node) + { + string uri = $"{(HttpSettings.UseHttps ? "https" : "http")}://{node.Hostname}:{node.Port ?? HttpSettings.BasePort}/{ServiceName}.{request.Target.MethodName}"; + + return new HttpRequestMessage(HttpMethod.Post, uri) + { + Content = new StringContent(JsonConvert.SerializeObject(request, jsonSettings), Encoding.UTF8, "application/json") + { + Headers = { { GigyaHttpHeaders.ProtocolVersion, HttpServiceRequest.ProtocolVersion } } + } + }; + } + + // TBD: What do we do if different environment return different schemas? Should we return all of them, should we merge them? + public Task GetSchema() + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/Gigya.Microdot.ServiceProxy/ServiceProxyExtensions.cs b/Gigya.Microdot.ServiceProxy/ServiceProxyExtensions.cs index bc9b68f6..f02ee406 100644 --- a/Gigya.Microdot.ServiceProxy/ServiceProxyExtensions.cs +++ b/Gigya.Microdot.ServiceProxy/ServiceProxyExtensions.cs @@ -21,7 +21,6 @@ #endregion using System; -using Gigya.Common.Contracts.HttpService; namespace Gigya.Microdot.ServiceProxy { @@ -29,18 +28,28 @@ public static class ServiceProxyExtensions { public static string GetServiceName(this Type serviceInterfaceType) { - var attribute = (HttpServiceAttribute)Attribute.GetCustomAttribute(serviceInterfaceType, typeof(HttpServiceAttribute)); - if (attribute?.Name != null) - return attribute.Name; - var assemblyName = serviceInterfaceType.Assembly.GetName().Name; var endIndex = assemblyName.IndexOf(".Interface", StringComparison.OrdinalIgnoreCase); if (endIndex <= 0) - return serviceInterfaceType.FullName.Replace('+', '-'); + return GetServiceNameFromTypeName(serviceInterfaceType.Name) ?? + serviceInterfaceType.FullName.Replace('+', '-'); var startIndex = assemblyName.Substring(0, endIndex).LastIndexOf(".", StringComparison.OrdinalIgnoreCase) + 1; var length = endIndex - startIndex; return assemblyName.Substring(startIndex, length); } + + private static string GetServiceNameFromTypeName(string typeName) + { + // if typeName is starts with 'I' letter and the following letter is an upper-case letter, + // then it represents an interface name, like 'IDemoService', and the 'I' letter should be ignored. + if (typeName.Length > 1 && typeName[0] == 'I' && typeName[1] >= 'A' && typeName[1] <= 'Z') + typeName = typeName.Substring(1); + + if (typeName.EndsWith("Service")) + return typeName; + else + return null; + } } } diff --git a/Gigya.Microdot.ServiceProxy/ServiceProxyProvider.cs b/Gigya.Microdot.ServiceProxy/ServiceProxyProvider.cs index 2deb9fd2..defbda9c 100644 --- a/Gigya.Microdot.ServiceProxy/ServiceProxyProvider.cs +++ b/Gigya.Microdot.ServiceProxy/ServiceProxyProvider.cs @@ -36,13 +36,13 @@ using Gigya.Common.Contracts.Exceptions; using Gigya.Common.Contracts.HttpService; using Gigya.Microdot.Interfaces.Events; -using Gigya.Microdot.Interfaces.HttpService; using Gigya.Microdot.Interfaces.Logging; using Gigya.Microdot.ServiceDiscovery; using Gigya.Microdot.ServiceDiscovery.Config; using Gigya.Microdot.SharedLogic; using Gigya.Microdot.SharedLogic.Events; using Gigya.Microdot.SharedLogic.Exceptions; +using Gigya.Microdot.SharedLogic.HttpService; using Gigya.Microdot.SharedLogic.Security; using Metrics; using Newtonsoft.Json; @@ -65,8 +65,7 @@ public class ServiceProxyProvider : IDisposable, IServiceProxyProvider public int? DefaultPort { get; set; } /// - /// Gets the name of the remote service. This defaults to the friendly name that was specified in the - /// decorating TInterface. If none were specified, the interface name + /// Gets the name of the remote service from the interface name. /// is used. /// public string ServiceName { get; } @@ -76,7 +75,7 @@ public class ServiceProxyProvider : IDisposable, IServiceProxyProvider /// value that was specified in the decorating TInterface, overridden /// by service discovery. /// - public bool UseHttpsDefault { get; set; } + public bool UseHttpsDefault { get; set; } /// @@ -124,7 +123,7 @@ protected internal HttpMessageHandler HttpMessageHandler public const string METRICS_CONTEXT_NAME = "ServiceProxy"; private ICertificateLocator CertificateLocator { get; } - + private ILog Log { get; } private ServiceDiscoveryConfig GetConfig() => GetDiscoveryConfig().Services[ServiceName]; private Func GetDiscoveryConfig { get; } @@ -147,7 +146,7 @@ public ServiceProxyProvider(string serviceName, IEventPublisher { EventPublisher = eventPublisher; CertificateLocator = certificateLocator; - + Log = log; ServiceName = serviceName; @@ -227,14 +226,14 @@ private void InitHttps(string securityRole) return false; case SslPolicyErrors.RemoteCertificateChainErrors: Log.Error(log => - { - var sb = new StringBuilder("Certificate error/s."); - foreach (var chainStatus in serverChain.ChainStatus) - { - sb.AppendFormat("Status {0}, status information {1}\n", chainStatus.Status, chainStatus.StatusInformation); - } - log(sb.ToString()); - }); + { + var sb = new StringBuilder("Certificate error/s."); + foreach (var chainStatus in serverChain.ChainStatus) + { + sb.AppendFormat("Status {0}, status information {1}\n", chainStatus.Status, chainStatus.StatusInformation); + } + log(sb.ToString()); + }); return false; case SslPolicyErrors.RemoteCertificateNameMismatch: // by design domain name do not match name of certificate, so RemoteCertificateNameMismatch is not an error. case SslPolicyErrors.None: @@ -308,18 +307,18 @@ public virtual async Task Invoke(HttpServiceRequest request, Type result private async Task InvokeCore(HttpServiceRequest request, Type resultReturnType, JsonSerializerSettings jsonSettings) { - if(request == null) + if (request == null) throw new ArgumentNullException(nameof(request)); request.Overrides = TracingContext.TryGetOverrides(); request.TracingData = new TracingData { - HostName = CurrentApplicationInfo.HostName?.ToUpperInvariant(), - ServiceName = CurrentApplicationInfo.Name, - RequestID = TracingContext.TryGetRequestID(), - SpanID = Guid.NewGuid().ToString("N"), //Each call is new span - ParentSpanID = TracingContext.TryGetSpanID(), - SpanStartTime = DateTimeOffset.UtcNow, - AbandonRequestBy = TracingContext.AbandonRequestBy, + HostName = CurrentApplicationInfo.HostName?.ToUpperInvariant(), + ServiceName = CurrentApplicationInfo.Name, + RequestID = TracingContext.TryGetRequestID(), + SpanID = Guid.NewGuid().ToString("N"), //Each call is new span + ParentSpanID = TracingContext.TryGetSpanID(), + SpanStartTime = DateTimeOffset.UtcNow, + AbandonRequestBy = TracingContext.AbandonRequestBy }; PrepareRequest?.Invoke(request); var requestContent = _serializationTime.Time(() => JsonConvert.SerializeObject(request, jsonSettings)); @@ -329,7 +328,7 @@ private async Task InvokeCore(HttpServiceRequest request, Type resultRet var config = GetConfig(); var clientCallEvent = EventPublisher.CreateEvent(); clientCallEvent.TargetService = ServiceName; - clientCallEvent.RequestId = request.TracingData?.RequestID; + clientCallEvent.RequestId = request.TracingData?.RequestID; clientCallEvent.TargetMethod = request.Target.MethodName; clientCallEvent.SpanId = request.TracingData?.SpanID; clientCallEvent.ParentSpanId = request.TracingData?.ParentSpanID; @@ -347,7 +346,7 @@ private async Task InvokeCore(HttpServiceRequest request, Type resultRet // The URL is only for a nice experience in Fiddler, it's never parsed/used for anything. var uri = BuildUri(endPoint.HostName, effectivePort.Value, config) + ServiceName; - if (request.Target.MethodName!=null) + if (request.Target.MethodName != null) uri += $".{request.Target.MethodName}"; if (request.Target.Endpoint != null) uri += $"/{request.Target.Endpoint}"; @@ -367,7 +366,7 @@ private async Task InvokeCore(HttpServiceRequest request, Type resultRet clientCallEvent.TargetPort = effectivePort.Value; var httpContent = new StringContent(requestContent, Encoding.UTF8, "application/json"); - httpContent.Headers.Add(GigyaHttpHeaders.Version, HttpServiceRequest.Version); + httpContent.Headers.Add(GigyaHttpHeaders.ProtocolVersion, HttpServiceRequest.ProtocolVersion); clientCallEvent.RequestStartTimestamp = Stopwatch.GetTimestamp(); try @@ -393,7 +392,7 @@ private async Task InvokeCore(HttpServiceRequest request, Type resultRet Log.Error("The remote service failed to return a valid HTTP response. Continuing to next " + "host. See tags for URL and exception for details.", exception: ex, - unencryptedTags: new {uri}); + unencryptedTags: new { uri }); endPoint.ReportFailure(ex); _hostFailureCounter.Increment("RequestFailure"); @@ -422,13 +421,13 @@ private async Task InvokeCore(HttpServiceRequest request, Type resultRet throw rex; } - if (response.Headers.Contains(GigyaHttpHeaders.ServerHostname) || response.Headers.Contains(GigyaHttpHeaders.Version)) + if (response.Headers.Contains(GigyaHttpHeaders.ServerHostname) || response.Headers.Contains(GigyaHttpHeaders.ProtocolVersion)) { try { endPoint.ReportSuccess(); - if(response.IsSuccessStatusCode) + if (response.IsSuccessStatusCode) { var returnObj = _deserializationTime.Time(() => JsonConvert.DeserializeObject(responseContent, resultReturnType, jsonSettings)); @@ -446,7 +445,7 @@ private async Task InvokeCore(HttpServiceRequest request, Type resultRet { remoteException = _deserializationTime.Time(() => ExceptionSerializer.Deserialize(responseContent)); } - catch(Exception ex) + catch (Exception ex) { _applicationExceptionCounter.Increment("ExceptionDeserializationFailure"); @@ -456,8 +455,8 @@ private async Task InvokeCore(HttpServiceRequest request, Type resultRet "'responseContent' encrypted tag for the original response content.", uri, ex, - unencrypted: new Tags {{"requestUri", uri}}, - encrypted: new Tags {{"responseContent", responseContent}}); + unencrypted: new Tags { { "requestUri", uri } }, + encrypted: new Tags { { "responseContent", responseContent } }); } _applicationExceptionCounter.Increment(); @@ -465,10 +464,10 @@ private async Task InvokeCore(HttpServiceRequest request, Type resultRet clientCallEvent.Exception = remoteException; EventPublisher.TryPublish(clientCallEvent); // fire and forget! - if(remoteException is RequestException || remoteException is EnvironmentException) + if (remoteException is RequestException || remoteException is EnvironmentException) ExceptionDispatchInfo.Capture(remoteException).Throw(); - if(remoteException is UnhandledException) + if (remoteException is UnhandledException) remoteException = remoteException.InnerException; throw new RemoteServiceException("The remote service returned a failure response. See " + @@ -476,7 +475,7 @@ private async Task InvokeCore(HttpServiceRequest request, Type resultRet "inner exception for details.", uri, remoteException, - unencrypted: new Tags {{"requestUri", uri}}); + unencrypted: new Tags { { "requestUri", uri } }); } } catch (JsonException ex) @@ -487,8 +486,8 @@ private async Task InvokeCore(HttpServiceRequest request, Type resultRet "deserialization. See the 'uri' tag for the URL that was called, the exception for the " + "exact error and the 'responseContent' encrypted tag for the original response content.", exception: ex, - unencryptedTags: new {uri}, - encryptedTags: new {responseContent}); + unencryptedTags: new { uri }, + encryptedTags: new { responseContent }); clientCallEvent.Exception = ex; EventPublisher.TryPublish(clientCallEvent); // fire and forget! @@ -498,23 +497,23 @@ private async Task InvokeCore(HttpServiceRequest request, Type resultRet "encrypted tag for the original response content.", uri, ex, - new Tags {{"responseContent", responseContent}}, - new Tags {{"requestUri", uri}}); + new Tags { { "responseContent", responseContent } }, + new Tags { { "requestUri", uri } }); } } else { var exception = response.StatusCode == HttpStatusCode.ServiceUnavailable ? - new Exception($"The remote service is unavailable (503) and is not recognized as a Gigya host at uri: {uri}"): + new Exception($"The remote service is unavailable (503) and is not recognized as a Gigya host at uri: {uri}") : new Exception($"The remote service returned a response but is not recognized as a Gigya host at uri: {uri}"); endPoint.ReportFailure(exception); _hostFailureCounter.Increment("NotGigyaHost"); - if(response.StatusCode == HttpStatusCode.ServiceUnavailable) - Log.Error("The remote service is unavailable (503) and is not recognized as a Gigya host. Continuing to next host.", unencryptedTags: new {uri}); + if (response.StatusCode == HttpStatusCode.ServiceUnavailable) + Log.Error("The remote service is unavailable (503) and is not recognized as a Gigya host. Continuing to next host.", unencryptedTags: new { uri }); else - Log.Error("The remote service returned a response but is not recognized as a Gigya host. Continuing to next host.", unencryptedTags: new {uri, statusCode = response.StatusCode}, encryptedTags: new {responseContent}); + Log.Error("The remote service returned a response but is not recognized as a Gigya host. Continuing to next host.", unencryptedTags: new { uri, statusCode = response.StatusCode }, encryptedTags: new { responseContent }); clientCallEvent.ErrCode = 500001;//(int)GSErrors.General_Server_Error; EventPublisher.TryPublish(clientCallEvent); // fire and forget! @@ -524,8 +523,8 @@ private async Task InvokeCore(HttpServiceRequest request, Type resultRet public async Task GetSchema() { - var result = await InvokeCore(new HttpServiceRequest {Target = new InvocationTarget {Endpoint = "schema"}}, typeof(ServiceSchema), JsonSettings).ConfigureAwait(false); - return (ServiceSchema) result; + var result = await InvokeCore(new HttpServiceRequest { Target = new InvocationTarget { Endpoint = "schema" } }, typeof(ServiceSchema), JsonSettings).ConfigureAwait(false); + return (ServiceSchema)result; } public void Dispose() diff --git a/Gigya.Microdot.ServiceProxy/ServiceProxyProviderGeneric.cs b/Gigya.Microdot.ServiceProxy/ServiceProxyProviderGeneric.cs index c6188752..ed5e0a49 100644 --- a/Gigya.Microdot.ServiceProxy/ServiceProxyProviderGeneric.cs +++ b/Gigya.Microdot.ServiceProxy/ServiceProxyProviderGeneric.cs @@ -26,8 +26,8 @@ using System.Reflection.DispatchProxy; using Gigya.Common.Contracts.Exceptions; using Gigya.Common.Contracts.HttpService; -using Gigya.Microdot.Interfaces.HttpService; using Gigya.Microdot.SharedLogic; +using Gigya.Microdot.SharedLogic.HttpService; namespace Gigya.Microdot.ServiceProxy { diff --git a/Gigya.Microdot.SharedLogic/Events/Event.cs b/Gigya.Microdot.SharedLogic/Events/Event.cs index 85915cf8..394709dc 100644 --- a/Gigya.Microdot.SharedLogic/Events/Event.cs +++ b/Gigya.Microdot.SharedLogic/Events/Event.cs @@ -28,6 +28,7 @@ using Gigya.Microdot.Interfaces.Configuration; using Gigya.Microdot.Interfaces.Events; using Gigya.Microdot.Interfaces.Logging; +using Gigya.Microdot.Interfaces.SystemWrappers; using Gigya.Microdot.SharedLogic.Logging; using Gigya.Microdot.SharedLogic.Utils; @@ -46,7 +47,7 @@ namespace Gigya.Microdot.SharedLogic.Events /// public class Event : IEvent { - public IEnvironmentVariableProvider EnvironmentVariableProvider { get; set; } + public IEnvironment Environment { get; set; } public IStackTraceEnhancer StackTraceEnhancer { get; set; } public EventConfiguration Configuration { get; set; } @@ -88,13 +89,22 @@ public class Event : IEvent [EventField(EventConsts.infrVersion, OmitFromAudit = true)] public string InfraVersion => CurrentApplicationInfo.InfraVersion.ToString(4); - /// The value of the %ENV% environment variable. - [EventField(EventConsts.runtimeENV, OmitFromAudit = true)] - public string RuntimeENV => EnvironmentVariableProvider.DeploymentEnvironment; + /// The value of the %REGION% environment variable. . + [EventField(EventConsts.runtimeREGION, OmitFromAudit = true)] + public string RuntimeRegion => Environment.Region; + + /// The value of the %REGION% environment variable. . + [EventField(EventConsts.runtimeZONE, OmitFromAudit = true)] + public string RuntimeZone => Environment.Zone; /// The value of the %DC% environment variable. . [EventField(EventConsts.runtimeDC, OmitFromAudit = true)] - public string RuntimeDC => EnvironmentVariableProvider.DataCenter; + [Obsolete("Deprecate after 2018; use region instead")] + public string RuntimeDC => Environment.Zone; + + /// The value of the %ENV% environment variable. + [EventField(EventConsts.runtimeENV, OmitFromAudit = true)] + public string RuntimeENV => Environment.DeploymentEnvironment; ///// The hostname of the server making the report [EventField(EventConsts.runtimeHost)] diff --git a/Gigya.Microdot.SharedLogic/Events/EventConsts.cs b/Gigya.Microdot.SharedLogic/Events/EventConsts.cs index ef4d7ec0..03a04c80 100644 --- a/Gigya.Microdot.SharedLogic/Events/EventConsts.cs +++ b/Gigya.Microdot.SharedLogic/Events/EventConsts.cs @@ -68,6 +68,8 @@ public static class EventConsts public const string clnSendTimestamp = "cln.sendTimestamp"; public const string runtimeHost = "runtime.host"; + public const string runtimeREGION = "runtime.region"; + public const string runtimeZONE = "runtime.zone"; public const string runtimeDC = "runtime.dc"; public const string runtimeENV = "runtime.env"; diff --git a/Gigya.Microdot.SharedLogic/Events/EventSerializer.cs b/Gigya.Microdot.SharedLogic/Events/EventSerializer.cs index 8cbc2970..de51b617 100644 --- a/Gigya.Microdot.SharedLogic/Events/EventSerializer.cs +++ b/Gigya.Microdot.SharedLogic/Events/EventSerializer.cs @@ -9,6 +9,7 @@ using Gigya.Microdot.Interfaces.Configuration; using Gigya.Microdot.Interfaces.Events; using Gigya.Microdot.Interfaces.Logging; +using Gigya.Microdot.Interfaces.SystemWrappers; namespace Gigya.Microdot.SharedLogic.Events { @@ -26,16 +27,16 @@ private class MemberToSerialize private Func LoggingConfigFactory { get; } - private IEnvironmentVariableProvider EnvProvider { get; } + private IEnvironment Environment { get; } private IStackTraceEnhancer StackTraceEnhancer { get; } private Func EventConfig { get; } public EventSerializer(Func loggingConfigFactory, - IEnvironmentVariableProvider envProvider, IStackTraceEnhancer stackTraceEnhancer, Func eventConfig) + IEnvironment environment, IStackTraceEnhancer stackTraceEnhancer, Func eventConfig) { LoggingConfigFactory = loggingConfigFactory; - EnvProvider = envProvider; + Environment = environment; StackTraceEnhancer = stackTraceEnhancer; EventConfig = eventConfig; } @@ -45,7 +46,7 @@ public EventSerializer(Func loggingConfigFactory, public IEnumerable Serialize(IEvent evt, Func predicate = null) { evt.Configuration = LoggingConfigFactory(); - evt.EnvironmentVariableProvider = EnvProvider; + evt.Environment = Environment; evt.StackTraceEnhancer = StackTraceEnhancer; foreach (var member in GetMembersToSerialize(evt.GetType())) diff --git a/Gigya.Microdot.SharedLogic/Events/TracingContext.cs b/Gigya.Microdot.SharedLogic/Events/TracingContext.cs index c442350b..f9a1936e 100644 --- a/Gigya.Microdot.SharedLogic/Events/TracingContext.cs +++ b/Gigya.Microdot.SharedLogic/Events/TracingContext.cs @@ -24,7 +24,7 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.Remoting.Messaging; -using Gigya.Microdot.Interfaces.HttpService; +using Gigya.Microdot.SharedLogic.HttpService; namespace Gigya.Microdot.SharedLogic.Events { @@ -50,7 +50,11 @@ internal static RequestOverrides TryGetOverrides() return TryGetValue(OVERRIDES_KEY); } - + /// + /// Retrieves the host override for the specified service, or returns null if no override was set. + /// + /// The name of the service for which to retrieve the host override. + /// A instace with information about the overidden host for the specified service, or null if no override was set. public static HostOverride GetHostOverride(string serviceName) { return TryGetValue(OVERRIDES_KEY) @@ -82,7 +86,7 @@ public static void SetHostOverride(string serviceName, string host,int? port=nul overrides.Hosts.Add(hostOverride); } - hostOverride.Host = host; + hostOverride.Hostname = host; hostOverride.Port = port; } diff --git a/Gigya.Microdot.SharedLogic/Exceptions/StackTraceEnhancer.cs b/Gigya.Microdot.SharedLogic/Exceptions/StackTraceEnhancer.cs index 4b96e7be..ff66dce4 100644 --- a/Gigya.Microdot.SharedLogic/Exceptions/StackTraceEnhancer.cs +++ b/Gigya.Microdot.SharedLogic/Exceptions/StackTraceEnhancer.cs @@ -5,6 +5,7 @@ using Gigya.Common.Contracts.Exceptions; using Gigya.Microdot.Interfaces.Configuration; using Gigya.Microdot.Interfaces.Logging; +using Gigya.Microdot.Interfaces.SystemWrappers; using Gigya.ServiceContract.Exceptions; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -14,7 +15,7 @@ namespace Gigya.Microdot.SharedLogic.Exceptions public class StackTraceEnhancer : IStackTraceEnhancer { private Func GetConfig { get; } - private IEnvironmentVariableProvider EnvironmentVariableProvider { get; } + private IEnvironment Environment { get; } private static readonly JsonSerializer Serializer = new JsonSerializer { TypeNameHandling = TypeNameHandling.All, @@ -24,10 +25,10 @@ public class StackTraceEnhancer : IStackTraceEnhancer Converters = { new StripHttpRequestExceptionConverter() } }; - public StackTraceEnhancer(Func getConfig, IEnvironmentVariableProvider environmentVariableProvider) + public StackTraceEnhancer(Func getConfig, IEnvironment environment) { GetConfig = getConfig; - EnvironmentVariableProvider = environmentVariableProvider; + Environment = environment; } public JObject ToJObjectWithBreadcrumb(Exception exception) @@ -51,8 +52,8 @@ public JObject ToJObjectWithBreadcrumb(Exception exception) ServiceName = CurrentApplicationInfo.Name, ServiceVersion = CurrentApplicationInfo.Version.ToString(), HostName = CurrentApplicationInfo.HostName, - DataCenter = EnvironmentVariableProvider.DataCenter, - DeploymentEnvironment = EnvironmentVariableProvider.DeploymentEnvironment + DataCenter = Environment.Zone, + DeploymentEnvironment = Environment.DeploymentEnvironment }; if (exception is SerializableException serEx) diff --git a/Gigya.Microdot.SharedLogic/Gigya.Microdot.SharedLogic.csproj b/Gigya.Microdot.SharedLogic/Gigya.Microdot.SharedLogic.csproj index 2aadab6c..43f2f783 100644 --- a/Gigya.Microdot.SharedLogic/Gigya.Microdot.SharedLogic.csproj +++ b/Gigya.Microdot.SharedLogic/Gigya.Microdot.SharedLogic.csproj @@ -52,11 +52,16 @@ + + + + + @@ -143,6 +148,17 @@ + + + + + ..\packages\System.ValueTuple\lib\netstandard1.0\System.ValueTuple.dll + True + True + + + + @@ -154,4 +170,5 @@ + \ No newline at end of file diff --git a/Gigya.Microdot.SharedLogic/GigyaHttpHeaders.cs b/Gigya.Microdot.SharedLogic/GigyaHttpHeaders.cs index 243da61c..6b425537 100644 --- a/Gigya.Microdot.SharedLogic/GigyaHttpHeaders.cs +++ b/Gigya.Microdot.SharedLogic/GigyaHttpHeaders.cs @@ -23,11 +23,12 @@ namespace Gigya.Microdot.SharedLogic { public static class GigyaHttpHeaders { - public const string Version = "X-Gigya-ProtocolVersion"; + public const string ProtocolVersion = "X-Gigya-ProtocolVersion"; public const string ServerHostname = "X-Gigya-ServerHostname"; public const string ExecutionTime = "X-Gigya-ExecutionTime"; public const string DataCenter = "X-Gigya-DC"; public const string Environment = "X-Gigya-ENV"; public const string ServiceVersion = "X-Gigya-ServiceVersion"; + public const string SchemaHash = "X-Gigya-SchemaHash"; } } diff --git a/Gigya.Microdot.Interfaces/HttpService/HttpServiceRequest.cs b/Gigya.Microdot.SharedLogic/HttpService/HttpServiceRequest.cs similarity index 79% rename from Gigya.Microdot.Interfaces/HttpService/HttpServiceRequest.cs rename to Gigya.Microdot.SharedLogic/HttpService/HttpServiceRequest.cs index d83ce876..ec7fbed3 100644 --- a/Gigya.Microdot.Interfaces/HttpService/HttpServiceRequest.cs +++ b/Gigya.Microdot.SharedLogic/HttpService/HttpServiceRequest.cs @@ -21,18 +21,20 @@ #endregion using System; +using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using System.Reflection; using System.Text.RegularExpressions; +using Gigya.Microdot.SharedLogic.Events; using Newtonsoft.Json; -namespace Gigya.Microdot.Interfaces.HttpService +namespace Gigya.Microdot.SharedLogic.HttpService { public class HttpServiceRequest { - public const string Version = "1"; + public const string ProtocolVersion = "1"; [JsonProperty(Order = 0)] public OrderedDictionary Arguments { get; set; } @@ -46,60 +48,52 @@ public class HttpServiceRequest [JsonProperty(Order = 3)] public InvocationTarget Target { get; set; } + /// + /// Constructor for deserialization. Should not set any property values. + /// + public HttpServiceRequest() { } - public HttpServiceRequest() { } - - public HttpServiceRequest(string targetMethod, string typeName, Dictionary arguments) + private HttpServiceRequest(ICollection arguments) { - if (targetMethod == null) - throw new ArgumentNullException(nameof(targetMethod)); - if (arguments == null) throw new ArgumentNullException(nameof(arguments)); - - Target = new InvocationTarget(targetMethod, typeName); - Arguments = new OrderedDictionary(); - - foreach(var argument in arguments) - { - Arguments.Add(argument.Key, argument.Value); - } + Arguments = new OrderedDictionary(arguments.Count); } - public HttpServiceRequest(string targetMethod, Dictionary arguments) + /// + /// Used for weakly-typed requests. Arguments sent in this way are matched by name only, the order isn't used. + /// + /// The name of the method to be called. + /// The type on which to call the method. + /// A dictionary of arguments to be supplied to the method, where the key is the name of the argument. + public HttpServiceRequest(string targetMethod, string typeName, Dictionary arguments) : this(arguments) { if (targetMethod == null) throw new ArgumentNullException(nameof(targetMethod)); - if (arguments == null) - throw new ArgumentNullException(nameof(arguments)); - - - Target = new InvocationTarget(targetMethod); - Arguments = new OrderedDictionary(); + Target = new InvocationTarget(targetMethod, typeName); - foreach (var argument in arguments) - { + foreach(var argument in arguments) Arguments.Add(argument.Key, argument.Value); - } } - public HttpServiceRequest(MethodInfo targetMethod, object[] arguments) + /// + /// Used for strongly-typed requests. Arguments sent in this way are matched by order only, the name isn't used. + /// + /// The of the method to be called. + /// An array of arguments to be supplied to the method, where the position of the elements match the position of the arguments. + public HttpServiceRequest(MethodInfo targetMethod, object[] arguments) : this(arguments) { if (targetMethod == null) throw new ArgumentNullException(nameof(targetMethod)); - if (arguments == null) - throw new ArgumentNullException(nameof(arguments)); - var parameters = targetMethod.GetParameters(); if (arguments.Length < parameters.Count(a => a.IsOptional == false)) throw new ArgumentException("An incorrect number of arguments was supplied for the specified target method.", nameof(arguments)); - Target = new InvocationTarget(targetMethod); - Arguments = new OrderedDictionary(arguments.Length); + Target = new InvocationTarget(targetMethod, parameters); for (int i = 0; i < arguments.Length; i++) @@ -124,22 +118,17 @@ public class InvocationTarget public InvocationTarget() { } - public InvocationTarget(string methodName, string typeName=null) + public InvocationTarget(string methodName, string typeName = null) { MethodName = methodName; TypeName = typeName; } - public InvocationTarget(MethodInfo method):this(method,null) - { - - } - public InvocationTarget(MethodInfo method, ParameterInfo[] parameterTypes) { TypeName = method.DeclaringType.FullName; MethodName = method.Name; - ParameterTypes = (parameterTypes ?? method.GetParameters()).Select(p => GetCleanTypeName(p.ParameterType)).ToArray(); + ParameterTypes = parameterTypes.Select(p => GetCleanTypeName(p.ParameterType)).ToArray(); } diff --git a/Gigya.Microdot.Interfaces/HttpService/ICertificateLocator.cs b/Gigya.Microdot.SharedLogic/HttpService/ICertificateLocator.cs similarity index 96% rename from Gigya.Microdot.Interfaces/HttpService/ICertificateLocator.cs rename to Gigya.Microdot.SharedLogic/HttpService/ICertificateLocator.cs index 69029df9..6609adee 100644 --- a/Gigya.Microdot.Interfaces/HttpService/ICertificateLocator.cs +++ b/Gigya.Microdot.SharedLogic/HttpService/ICertificateLocator.cs @@ -22,7 +22,7 @@ using System.Security.Cryptography.X509Certificates; -namespace Gigya.Microdot.Interfaces.HttpService +namespace Gigya.Microdot.SharedLogic.HttpService { public interface ICertificateLocator { diff --git a/Gigya.Microdot.Interfaces/HttpService/RequestOverrides.cs b/Gigya.Microdot.SharedLogic/HttpService/RequestOverrides.cs similarity index 90% rename from Gigya.Microdot.Interfaces/HttpService/RequestOverrides.cs rename to Gigya.Microdot.SharedLogic/HttpService/RequestOverrides.cs index c8980305..cf342ef7 100644 --- a/Gigya.Microdot.Interfaces/HttpService/RequestOverrides.cs +++ b/Gigya.Microdot.SharedLogic/HttpService/RequestOverrides.cs @@ -22,9 +22,10 @@ using System; using System.Collections.Generic; +using Gigya.Microdot.SharedLogic.Rewrite; using Newtonsoft.Json; -namespace Gigya.Microdot.Interfaces.HttpService +namespace Gigya.Microdot.SharedLogic.HttpService { [Serializable] public class RequestOverrides @@ -34,13 +35,13 @@ public class RequestOverrides } [Serializable] - public class HostOverride + public class HostOverride { [JsonProperty] public string ServiceName { get; set; } [JsonProperty] - public string Host { get; set; } + public string Hostname { get; set; } [JsonProperty] public int? Port { get; set; } diff --git a/Gigya.Microdot.Interfaces/HttpService/ServiceReachabilityStatus.cs b/Gigya.Microdot.SharedLogic/HttpService/ServiceReachabilityStatus.cs similarity index 96% rename from Gigya.Microdot.Interfaces/HttpService/ServiceReachabilityStatus.cs rename to Gigya.Microdot.SharedLogic/HttpService/ServiceReachabilityStatus.cs index b712e4bd..c5841d7b 100644 --- a/Gigya.Microdot.Interfaces/HttpService/ServiceReachabilityStatus.cs +++ b/Gigya.Microdot.SharedLogic/HttpService/ServiceReachabilityStatus.cs @@ -19,7 +19,7 @@ // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE // POSSIBILITY OF SUCH DAMAGE. #endregion -namespace Gigya.Microdot.Interfaces.HttpService +namespace Gigya.Microdot.SharedLogic.HttpService { public class ServiceReachabilityStatus { diff --git a/Gigya.Microdot.SharedLogic/Monitor/AggregatingHealthStatus.cs b/Gigya.Microdot.SharedLogic/Monitor/AggregatingHealthStatus.cs index 1efd984d..d96bb184 100644 --- a/Gigya.Microdot.SharedLogic/Monitor/AggregatingHealthStatus.cs +++ b/Gigya.Microdot.SharedLogic/Monitor/AggregatingHealthStatus.cs @@ -22,6 +22,7 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Linq; using Metrics; @@ -29,7 +30,8 @@ namespace Gigya.Microdot.SharedLogic.Monitor { public class AggregatingHealthStatus { - private readonly ConcurrentDictionary> _checks = new ConcurrentDictionary>(); + private readonly List _checks = new List(); + private readonly object _locker = new object(); public AggregatingHealthStatus(string componentName, IHealthMonitor healthMonitor) { @@ -38,27 +40,65 @@ public AggregatingHealthStatus(string componentName, IHealthMonitor healthMonito private HealthCheckResult HealthCheck() { - var results =_checks - .Select(c => new {c.Key, Result = c.Value()}) + DisposableHealthCheck[] checks; + lock (_locker) + { + checks = _checks.ToArray(); // get the current state of the health-checks list + } + + // don't call the health check functions inside a lock. It may run for a long time, + // and in the worse case it may cause a dead-lock, if the function is locking something else that we depend on + var results = checks + .Select(c => new { c.Name, Result = c.CheckFunc() }) .OrderBy(c => c.Result.IsHealthy) - .ThenBy(c => c.Key) + .ThenBy(c => c.Name) .ToArray(); bool healthy = results.All(r => r.Result.IsHealthy); - string message = string.Join("\r\n", results.Select(r => (r.Result.IsHealthy ? "[OK] " : "[Unhealthy] ") + r.Result.Message)); + string message = string.Join(Environment.NewLine, results.Select(r => $"{(r.Result.IsHealthy ? "[OK]" : "[Unhealthy]")} {r.Name} - {r.Result.Message}")); return healthy ? HealthCheckResult.Healthy(message) : HealthCheckResult.Unhealthy(message); } + public IDisposable RegisterCheck(string name, Func checkFunc) + { + lock (_locker) + { + var healthCheck = new DisposableHealthCheck(name, checkFunc, RemoveCheck); + _checks.Add(healthCheck); + return healthCheck; + } + } - public void RegisterCheck(string name, Func checkFunc) + private void RemoveCheck(DisposableHealthCheck healthCheck) { - _checks.AddOrUpdate(name, checkFunc, (a, b) => checkFunc); + lock (_locker) + { + _checks.Remove(healthCheck); + } } - public void RemoveCheck(string name) + private class DisposableHealthCheck : IDisposable { - _checks.TryRemove(name, out var _); + private readonly Action _disposed; + public string Name { get; } + public Func CheckFunc { get; private set; } + + public DisposableHealthCheck(string name, Func checkFunc, Action whenDisposed) + { + _disposed = whenDisposed; + Name = name; + CheckFunc = checkFunc; + } + + public void Dispose() + { + _disposed(this); + CheckFunc = null; + } } + + } + } \ No newline at end of file diff --git a/Gigya.Microdot.SharedLogic/Rewrite/Node.cs b/Gigya.Microdot.SharedLogic/Rewrite/Node.cs new file mode 100644 index 00000000..d94f46c5 --- /dev/null +++ b/Gigya.Microdot.SharedLogic/Rewrite/Node.cs @@ -0,0 +1,36 @@ +namespace Gigya.Microdot.SharedLogic.Rewrite +{ + public class Node + { + public Node(string hostName, int? port = null) + { + Hostname = hostName; + Port = port; + } + + public string Hostname { get; } + public int? Port { get; } + + public override string ToString() + { + return Port.HasValue ? $"{Hostname}:{Port}" : Hostname; + } + + public override bool Equals(object obj) + { + if (!(obj is Node other)) + return false; + + return other.Hostname == Hostname && other.Port == Port; + } + + + public override int GetHashCode() + { + unchecked + { + return ((Hostname?.GetHashCode() ?? 0) * 397) ^ (Port?.GetHashCode() ?? 1); + } + } + } +} diff --git a/Gigya.Microdot.SharedLogic/Security/WindowsStoreCertificateLocator.cs b/Gigya.Microdot.SharedLogic/Security/WindowsStoreCertificateLocator.cs index 3c46fc57..a3fad7de 100644 --- a/Gigya.Microdot.SharedLogic/Security/WindowsStoreCertificateLocator.cs +++ b/Gigya.Microdot.SharedLogic/Security/WindowsStoreCertificateLocator.cs @@ -26,8 +26,8 @@ using System.Linq; using System.Security.Cryptography.X509Certificates; using Gigya.Microdot.Interfaces.Configuration; -using Gigya.Microdot.Interfaces.HttpService; using Gigya.Microdot.SharedLogic.Exceptions; +using Gigya.Microdot.SharedLogic.HttpService; using Gigya.Microdot.SharedLogic.Utils; namespace Gigya.Microdot.SharedLogic.Security diff --git a/Gigya.Microdot.SharedLogic/SystemWrappers/DateTimeImpl.cs b/Gigya.Microdot.SharedLogic/SystemWrappers/DateTimeImpl.cs index 9a005368..e71cf5a3 100644 --- a/Gigya.Microdot.SharedLogic/SystemWrappers/DateTimeImpl.cs +++ b/Gigya.Microdot.SharedLogic/SystemWrappers/DateTimeImpl.cs @@ -21,14 +21,28 @@ #endregion using System; +using System.Threading; using System.Threading.Tasks; using Gigya.Microdot.Interfaces.SystemWrappers; namespace Gigya.Microdot.SharedLogic.SystemWrappers { - public class DateTimeImpl: IDateTime + public class DateTimeImpl : IDateTime { public DateTime UtcNow => DateTime.UtcNow; - public Task Delay(TimeSpan delay) { return Task.Delay(delay); } + + public Task Delay(TimeSpan delay) => Delay(delay, default(CancellationToken)); + public Task Delay(TimeSpan delay, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.Delay(delay, cancellationToken); + } + + public async Task DelayUntil(DateTime until, CancellationToken cancellationToken = default(CancellationToken)) + { + TimeSpan delayTime = until - UtcNow; + + if (delayTime > TimeSpan.Zero) + await Delay(delayTime, cancellationToken).ConfigureAwait(false); + } } } diff --git a/Gigya.Microdot.SharedLogic/SystemWrappers/EnvironmentInstance.cs b/Gigya.Microdot.SharedLogic/SystemWrappers/EnvironmentInstance.cs index dcec9bc0..520d8c7a 100644 --- a/Gigya.Microdot.SharedLogic/SystemWrappers/EnvironmentInstance.cs +++ b/Gigya.Microdot.SharedLogic/SystemWrappers/EnvironmentInstance.cs @@ -21,27 +21,49 @@ #endregion using System; +using Gigya.Common.Contracts.Exceptions; +using Gigya.Microdot.Interfaces.Configuration; using Gigya.Microdot.Interfaces.SystemWrappers; namespace Gigya.Microdot.SharedLogic.SystemWrappers { + [ConfigurationRoot("dataCenters", RootStrategy.ReplaceClassNameWithPath)] + public class DataCentersConfig : IConfigObject + { + public string Current { get; set; } + } + public class EnvironmentInstance : IEnvironment { - public EnvironmentInstance() + private readonly IEnvironmentVariableProvider _environmentVariableProvider; + private readonly string _region; + private Func GetDataCentersConfig { get; } + + public EnvironmentInstance(IEnvironmentVariableProvider environmentVariableProvider, Func getDataCentersConfig) { - PlatformID = Environment.OSVersion.Platform; - PlatformSpecificPathPrefix = PlatformID == PlatformID.Unix ? "/etc" : "D:"; - } + _environmentVariableProvider = environmentVariableProvider; + GetDataCentersConfig = getDataCentersConfig; + Zone = environmentVariableProvider.GetEnvironmentVariable("ZONE") ?? environmentVariableProvider.GetEnvironmentVariable("DC"); + _region = environmentVariableProvider.GetEnvironmentVariable("REGION"); + DeploymentEnvironment = environmentVariableProvider.GetEnvironmentVariable("ENV"); + ConsulAddress = environmentVariableProvider.GetEnvironmentVariable("CONSUL"); - public PlatformID PlatformID { get; } + if (string.IsNullOrEmpty(Zone) || string.IsNullOrEmpty(DeploymentEnvironment)) + throw new EnvironmentException("One or more of the following environment variables, which are required, have not been set: %ZONE%, %ENV%"); + } - public string PlatformSpecificPathPrefix { get; } + public string Zone { get; } + public string Region => _region ?? GetDataCentersConfig().Current; // if environmentVariable %REGION% does not exist, take the region from DataCenters configuration (the region was previously called "DataCenter") + public string DeploymentEnvironment { get; } + public string ConsulAddress { get; } + [Obsolete("To be deleted on version 2.0")] public void SetEnvironmentVariableForProcess(string name, string value) { - Environment.SetEnvironmentVariable(name, value.ToLower(), EnvironmentVariableTarget.Process); + _environmentVariableProvider.SetEnvironmentVariableForProcess(name, value); } - public string GetEnvironmentVariable(string name) { return Environment.GetEnvironmentVariable(name)?.ToLower(); } + [Obsolete("To be deleted on version 2.0")] + public string GetEnvironmentVariable(string name) => _environmentVariableProvider.GetEnvironmentVariable(name); } } \ No newline at end of file diff --git a/Gigya.Microdot.SharedLogic/Utils/Extensions.cs b/Gigya.Microdot.SharedLogic/Utils/Extensions.cs index 7e8e76d5..23415489 100644 --- a/Gigya.Microdot.SharedLogic/Utils/Extensions.cs +++ b/Gigya.Microdot.SharedLogic/Utils/Extensions.cs @@ -24,6 +24,7 @@ using System.IO; using Gigya.Common.Contracts.Exceptions; + namespace Gigya.Microdot.SharedLogic.Utils { public static class StringExtensions @@ -91,6 +92,22 @@ public static string Right(this string value, int length) public static class Extensions { + public static bool TryDispose(this IDisposable disposable) + { + if (disposable == null) + return false; + + try + { + disposable.Dispose(); + return true; + } + catch + { + return false; + } + } + public static string RawMessage(this Exception ex) => (ex as SerializableException)?.RawMessage ?? ex.Message; diff --git a/Gigya.Microdot.SharedLogic/paket.references b/Gigya.Microdot.SharedLogic/paket.references index 604be596..38742c4f 100644 --- a/Gigya.Microdot.SharedLogic/paket.references +++ b/Gigya.Microdot.SharedLogic/paket.references @@ -1,4 +1,5 @@ Gigya.ServiceContract Newtonsoft.Json Metrics.NET -System.Threading.Tasks.Dataflow \ No newline at end of file +System.Threading.Tasks.Dataflow +System.ValueTuple \ No newline at end of file diff --git a/Gigya.Microdot.Testing.Shared/Service/ServiceTesterBase.cs b/Gigya.Microdot.Testing.Shared/Service/ServiceTesterBase.cs index 6b1e090b..8adcb3f7 100644 --- a/Gigya.Microdot.Testing.Shared/Service/ServiceTesterBase.cs +++ b/Gigya.Microdot.Testing.Shared/Service/ServiceTesterBase.cs @@ -30,6 +30,7 @@ using Gigya.Microdot.Interfaces.Logging; using Gigya.Microdot.Orleans.Hosting; using Gigya.Microdot.ServiceDiscovery; +using Gigya.Microdot.ServiceDiscovery.Rewrite; using Gigya.Microdot.ServiceProxy; using Gigya.Microdot.ServiceProxy.Caching; using Gigya.Microdot.SharedLogic; diff --git a/Gigya.Microdot.Testing.Shared/TestingKernel.cs b/Gigya.Microdot.Testing.Shared/TestingKernel.cs index cbd3b27b..9daf8e4d 100644 --- a/Gigya.Microdot.Testing.Shared/TestingKernel.cs +++ b/Gigya.Microdot.Testing.Shared/TestingKernel.cs @@ -23,6 +23,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net; using Gigya.Microdot.Configuration; using Gigya.Microdot.Fakes; using Gigya.Microdot.Fakes.Discovery; @@ -31,6 +32,7 @@ using Gigya.Microdot.Interfaces.Logging; using Gigya.Microdot.Ninject; using Gigya.Microdot.ServiceDiscovery; +using Gigya.Microdot.ServiceDiscovery.Rewrite; using Gigya.Microdot.SharedLogic; using Gigya.Microdot.SharedLogic.Monitor; using Ninject; @@ -45,6 +47,7 @@ namespace Gigya.Microdot.Testing.Shared public TestingKernel(Action additionalBindings = null, Dictionary mockConfig = null) { + ServicePointManager.DefaultConnectionLimit = 200; CurrentApplicationInfo.Init(APPNAME); @@ -52,6 +55,7 @@ public TestingKernel(Action additionalBindings = null, Dictionary().To(); Rebind().To().InSingletonScope(); + Rebind().To().InSingletonScope(); Rebind().To().InSingletonScope(); var locationsParserMock = Substitute.For(); locationsParserMock.ConfigFileDeclarations.Returns(Enumerable.Empty().ToArray()); diff --git a/Gigya.ServiceContract/Attributes/HttpServiceAttribute.cs b/Gigya.ServiceContract/Attributes/HttpServiceAttribute.cs index fd634feb..e2a7d842 100644 --- a/Gigya.ServiceContract/Attributes/HttpServiceAttribute.cs +++ b/Gigya.ServiceContract/Attributes/HttpServiceAttribute.cs @@ -29,9 +29,9 @@ public class HttpServiceAttribute : Attribute { /// /// This is the port number that the service will listen to for incoming HTTP requests. Other ports (used for - /// Orleans, Metrics.Net, etc) are opened at sequencial numbers from this base offset. + /// Orleans, Metrics.Net, etc) are opened at sequential numbers from this base offset. /// - public int BasePort { get; private set; } + public int BasePort { get; set; } public bool UseHttps { get; set; } @@ -40,6 +40,7 @@ public HttpServiceAttribute(int basePort) BasePort = basePort; } - public string Name { get; set; } - } + [Obsolete("This propery is no longer in use, and will be removed on Microdot version 2.0. Service name is now extracted from its interface's namespace.")] + public string Name { get; set; } + } } diff --git a/Sample/CalculatorService.Client/Program.cs b/Sample/CalculatorService.Client/Program.cs index d239bf4b..95ba8b2c 100644 --- a/Sample/CalculatorService.Client/Program.cs +++ b/Sample/CalculatorService.Client/Program.cs @@ -17,7 +17,8 @@ static void Main(string[] args) Environment.SetEnvironmentVariable("GIGYA_CONFIG_ROOT", Environment.CurrentDirectory); Environment.SetEnvironmentVariable("GIGYA_CONFIG_PATHS_FILE", ""); Environment.SetEnvironmentVariable("GIGYA_ENVVARS_FILE", Environment.CurrentDirectory); - Environment.SetEnvironmentVariable("DC", "global"); + Environment.SetEnvironmentVariable("REGION", "us1"); + Environment.SetEnvironmentVariable("ZONE", "us1a"); Environment.SetEnvironmentVariable("ENV", "dev"); CurrentApplicationInfo.Init("CalculatorService.Client"); diff --git a/Sample/CalculatorService.Orleans/CalculatorServiceHost.cs b/Sample/CalculatorService.Orleans/CalculatorServiceHost.cs index ceb26daa..06ac022f 100644 --- a/Sample/CalculatorService.Orleans/CalculatorServiceHost.cs +++ b/Sample/CalculatorService.Orleans/CalculatorServiceHost.cs @@ -13,7 +13,8 @@ static void Main(string[] args) Environment.SetEnvironmentVariable("GIGYA_CONFIG_ROOT", Environment.CurrentDirectory); Environment.SetEnvironmentVariable("GIGYA_CONFIG_PATHS_FILE", ""); Environment.SetEnvironmentVariable("GIGYA_ENVVARS_FILE", Environment.CurrentDirectory); - Environment.SetEnvironmentVariable("DC", "global"); + Environment.SetEnvironmentVariable("REGION", "us1"); + Environment.SetEnvironmentVariable("ZONE", "us1a"); Environment.SetEnvironmentVariable("ENV", "dev"); diff --git a/Sample/CalculatorService/CalculatorServiceHost.cs b/Sample/CalculatorService/CalculatorServiceHost.cs index ea99ba2a..b010a246 100644 --- a/Sample/CalculatorService/CalculatorServiceHost.cs +++ b/Sample/CalculatorService/CalculatorServiceHost.cs @@ -16,7 +16,8 @@ static void Main(string[] args) Environment.SetEnvironmentVariable("GIGYA_CONFIG_ROOT", Environment.CurrentDirectory); Environment.SetEnvironmentVariable("GIGYA_CONFIG_PATHS_FILE", ""); Environment.SetEnvironmentVariable("GIGYA_ENVVARS_FILE", Environment.CurrentDirectory); - Environment.SetEnvironmentVariable("DC", "global"); + Environment.SetEnvironmentVariable("REGION", "us1"); + Environment.SetEnvironmentVariable("ZONE", "us1a"); Environment.SetEnvironmentVariable("ENV", "dev"); diff --git a/paket.dependencies b/paket.dependencies index deb81db5..be27d5a9 100644 --- a/paket.dependencies +++ b/paket.dependencies @@ -7,6 +7,9 @@ copy_content_to_output_dir: always nuget Gigya.ServiceContract ~> 2.0 +# .Net +nuget System.ValueTuple + # Misc nuget Metrics.NET ~> 0.0 nuget Newtonsoft.Json >= 9 lowest_matching: true diff --git a/tests/Gigya.Microdot.Hosting.UnitTests/NonOrleansMicroService/MicroServiceTests.cs b/tests/Gigya.Microdot.Hosting.UnitTests/NonOrleansMicroService/MicroServiceTests.cs index c8d1cc0f..a16a6d46 100644 --- a/tests/Gigya.Microdot.Hosting.UnitTests/NonOrleansMicroService/MicroServiceTests.cs +++ b/tests/Gigya.Microdot.Hosting.UnitTests/NonOrleansMicroService/MicroServiceTests.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Threading.Tasks; using Gigya.Microdot.Fakes; using Gigya.Microdot.Hosting.Service; @@ -14,6 +15,14 @@ namespace Gigya.Microdot.Hosting.UnitTests.NonOrleansMicroService [TestFixture] public class MicroServiceTests { + [SetUp] + public void Setup() + { + Environment.SetEnvironmentVariable("GIGYA_CONFIG_ROOT", AppDomain.CurrentDomain.BaseDirectory, EnvironmentVariableTarget.Process); + Environment.SetEnvironmentVariable("REGION", "us1", EnvironmentVariableTarget.Process); + Environment.SetEnvironmentVariable("ZONE", "us1a", EnvironmentVariableTarget.Process); + Environment.SetEnvironmentVariable("ENV", "_Test", EnvironmentVariableTarget.Process); + } [Test] public async Task ShouldCallSelfHostServcie() { diff --git a/tests/Gigya.Microdot.Orleans.Hosting.UnitTests/AssemblyInitialize.cs b/tests/Gigya.Microdot.Orleans.Hosting.UnitTests/AssemblyInitialize.cs index 1ddd3474..1fbe54d3 100644 --- a/tests/Gigya.Microdot.Orleans.Hosting.UnitTests/AssemblyInitialize.cs +++ b/tests/Gigya.Microdot.Orleans.Hosting.UnitTests/AssemblyInitialize.cs @@ -45,7 +45,8 @@ public void SetUp() try { Environment.SetEnvironmentVariable("GIGYA_CONFIG_ROOT", AppDomain.CurrentDomain.BaseDirectory, EnvironmentVariableTarget.Process); - Environment.SetEnvironmentVariable("DC","_US", EnvironmentVariableTarget.Process); + Environment.SetEnvironmentVariable("REGION", "us1", EnvironmentVariableTarget.Process); + Environment.SetEnvironmentVariable("ZONE", "us1a", EnvironmentVariableTarget.Process); Environment.SetEnvironmentVariable("ENV", "_Test", EnvironmentVariableTarget.Process); kernel = new TestingKernel((kernel) => diff --git a/tests/Gigya.Microdot.Orleans.Hosting.UnitTests/CalculatorServiceTests.cs b/tests/Gigya.Microdot.Orleans.Hosting.UnitTests/CalculatorServiceTests.cs index 8eb8a5a2..63e54e64 100644 --- a/tests/Gigya.Microdot.Orleans.Hosting.UnitTests/CalculatorServiceTests.cs +++ b/tests/Gigya.Microdot.Orleans.Hosting.UnitTests/CalculatorServiceTests.cs @@ -28,9 +28,9 @@ using System.Threading.Tasks; using Gigya.Microdot.Fakes; using Gigya.Microdot.Interfaces; -using Gigya.Microdot.Interfaces.HttpService; using Gigya.Microdot.Orleans.Hosting.UnitTests.Microservice.CalculatorService; using Gigya.Microdot.ServiceProxy; +using Gigya.Microdot.SharedLogic.HttpService; using Gigya.Microdot.Testing.Service; using Gigya.Microdot.Testing.Shared; using Gigya.ServiceContract.Attributes; @@ -246,7 +246,7 @@ public async Task CallWeakRequestWith_NoParamsAndNoReturnTypeAndNoType() var dict = new Dictionary(); serviceProxy.DefaultPort = 6555; - var res = await serviceProxy.Invoke(new HttpServiceRequest("Do", dict), typeof(JObject)); + var res = await serviceProxy.Invoke(new HttpServiceRequest("Do", null, dict), typeof(JObject)); var json = (JToken)res; json.ShouldBe(null); } diff --git a/tests/Gigya.Microdot.Orleans.Hosting.UnitTests/Gigya.Microdot.Orleans.Hosting.UnitTests.csproj b/tests/Gigya.Microdot.Orleans.Hosting.UnitTests/Gigya.Microdot.Orleans.Hosting.UnitTests.csproj index a5915ede..7e045699 100644 --- a/tests/Gigya.Microdot.Orleans.Hosting.UnitTests/Gigya.Microdot.Orleans.Hosting.UnitTests.csproj +++ b/tests/Gigya.Microdot.Orleans.Hosting.UnitTests/Gigya.Microdot.Orleans.Hosting.UnitTests.csproj @@ -63,6 +63,7 @@ + diff --git a/tests/Gigya.Microdot.Orleans.Hosting.UnitTests/ServiceSchemaTests.cs b/tests/Gigya.Microdot.Orleans.Hosting.UnitTests/ServiceSchemaTests.cs new file mode 100644 index 00000000..7e4a8192 --- /dev/null +++ b/tests/Gigya.Microdot.Orleans.Hosting.UnitTests/ServiceSchemaTests.cs @@ -0,0 +1,72 @@ +#region Copyright +// Copyright 2017 Gigya Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +#endregion + +using System.Threading.Tasks; +using Gigya.Common.Contracts.HttpService; +using Gigya.Microdot.Orleans.Hosting.UnitTests.Microservice.CalculatorService; +using Newtonsoft.Json; +using NUnit.Framework; + +namespace Gigya.Microdot.Orleans.Hosting.UnitTests +{ + [TestFixture] + public class ServiceSchemaTests + { + [Test] + public void ReturnSameHashCodeForSameSchema() + { + var firstSchema = GetSchema(); + var secondSchema = GetSchema(); + Assert.AreNotEqual(firstSchema, secondSchema); + Assert.AreEqual(firstSchema.Hash, secondSchema.Hash); + } + + [Test] + public void ReturnDifferentHashCodeForDifferentSchema() + { + var firstSchema = GetSchema(); + var secondSchema = GetSchema(); + + Assert.AreNotEqual(firstSchema.Hash, secondSchema.Hash); + } + + [Test] + public void ReturnSameHashCodeAfterSerialization() + { + var firstSchema = GetSchema(); + var serialized = JsonConvert.SerializeObject(firstSchema); + var secondSchema = JsonConvert.DeserializeObject(serialized); + Assert.AreEqual(firstSchema.Hash, secondSchema.Hash); + } + + private ServiceSchema GetSchema() + { + return new ServiceSchema(new[]{typeof(TService)}); + } + } + + [HttpService(3579)] + public interface ITestService + { + Task DoNothing(string foo); + } +} diff --git a/tests/Gigya.Microdot.ServiceContract.UnitTests/ServiceSchemaTests.cs b/tests/Gigya.Microdot.ServiceContract.UnitTests/ServiceSchemaTests.cs index 1078095b..4ab837bc 100644 --- a/tests/Gigya.Microdot.ServiceContract.UnitTests/ServiceSchemaTests.cs +++ b/tests/Gigya.Microdot.ServiceContract.UnitTests/ServiceSchemaTests.cs @@ -53,7 +53,7 @@ class ResponseData public class SensitiveAttribute : Attribute {} - [HttpService(100, Name = "ServiceName")] + [HttpService(100)] internal interface ITestInterface { [PublicEndpoint("demo.doSomething")] @@ -77,7 +77,6 @@ public void TestSerialization() Assert.IsTrue(schema.Interfaces[0].Attributes[0].Attribute is HttpServiceAttribute); Assert.IsTrue(schema.Interfaces[0].Attributes[0].TypeName == typeof(HttpServiceAttribute).AssemblyQualifiedName); Assert.IsTrue((schema.Interfaces[0].Attributes[0].Attribute as HttpServiceAttribute).BasePort == 100); - Assert.IsTrue((schema.Interfaces[0].Attributes[0].Attribute as HttpServiceAttribute).Name == "ServiceName"); Assert.IsTrue(schema.Interfaces[0].Methods.Length == 1); Assert.IsTrue(schema.Interfaces[0].Methods[0].Name == nameof(ITestInterface.DoSomething)); Assert.IsTrue(schema.Interfaces[0].Methods[0].Attributes.Length == 1); diff --git a/tests/Gigya.Microdot.UnitTests/AssemblyInitialize.cs b/tests/Gigya.Microdot.UnitTests/AssemblyInitialize.cs index ea41e807..cdfabad5 100644 --- a/tests/Gigya.Microdot.UnitTests/AssemblyInitialize.cs +++ b/tests/Gigya.Microdot.UnitTests/AssemblyInitialize.cs @@ -21,6 +21,7 @@ #endregion using System; +using System.Net; using NUnit.Framework; [SetUpFixture] @@ -28,11 +29,12 @@ public class AssemblyInitialize { [OneTimeSetUp] public void SetUp() - { + { try { Environment.SetEnvironmentVariable("GIGYA_CONFIG_ROOT", AppDomain.CurrentDomain.BaseDirectory, EnvironmentVariableTarget.Process); - Environment.SetEnvironmentVariable("DC","_US", EnvironmentVariableTarget.Process); + Environment.SetEnvironmentVariable("REGION", "us1", EnvironmentVariableTarget.Process); + Environment.SetEnvironmentVariable("ZONE", "us1a", EnvironmentVariableTarget.Process); Environment.SetEnvironmentVariable("ENV", "_Test", EnvironmentVariableTarget.Process); } catch(Exception ex) diff --git a/tests/Gigya.Microdot.UnitTests/Caching/CachingProxyTests.cs b/tests/Gigya.Microdot.UnitTests/Caching/CachingProxyTests.cs index 3d6e4f7c..eaabe563 100644 --- a/tests/Gigya.Microdot.UnitTests/Caching/CachingProxyTests.cs +++ b/tests/Gigya.Microdot.UnitTests/Caching/CachingProxyTests.cs @@ -249,7 +249,7 @@ private async Task ResultlRevocableServiceShouldBe(string expectedResult,string } } - [HttpService(1234, Name="CachingTestService")] + [HttpService(1234)] public interface ICachingTestService { [Cached] diff --git a/tests/Gigya.Microdot.UnitTests/Caching/Host/SlowServiceHost.cs b/tests/Gigya.Microdot.UnitTests/Caching/Host/SlowServiceHost.cs index a5899564..68069074 100644 --- a/tests/Gigya.Microdot.UnitTests/Caching/Host/SlowServiceHost.cs +++ b/tests/Gigya.Microdot.UnitTests/Caching/Host/SlowServiceHost.cs @@ -9,6 +9,7 @@ using Gigya.Microdot.Ninject; using Gigya.Microdot.Ninject.Host; using Gigya.Microdot.ServiceDiscovery; +using Gigya.Microdot.ServiceDiscovery.Rewrite; using Gigya.Microdot.SharedLogic; using Ninject; using Ninject.Syntax; @@ -48,6 +49,7 @@ public SlowServiceHost(Action action = null) protected override void Configure(IKernel kernel, BaseCommonConfig commonConfig) { kernel.Rebind().To().InSingletonScope(); + kernel.Rebind().To().InSingletonScope(); kernel.Rebind().To().InSingletonScope(); action?.Invoke(kernel); kernel.Bind().To().InSingletonScope(); diff --git a/tests/Gigya.Microdot.UnitTests/Configuration/EnviromentVariablesFileReaderTests.cs b/tests/Gigya.Microdot.UnitTests/Configuration/EnviromentVariablesFileReaderTests.cs deleted file mode 100644 index 1e4935dd..00000000 --- a/tests/Gigya.Microdot.UnitTests/Configuration/EnviromentVariablesFileReaderTests.cs +++ /dev/null @@ -1,129 +0,0 @@ -using System; -using System.Collections.Generic; - -using Gigya.Microdot.Configuration; -using Gigya.Microdot.Interfaces.SystemWrappers; -using Gigya.Microdot.SharedLogic.Exceptions; - -using NSubstitute; - -using NUnit.Framework; - -using Shouldly; - -namespace Gigya.Microdot.UnitTests.Configuration -{ - public class EnviromentVariablesFileReaderTests - { - private IFileSystem _fileSystem; - private IEnvironment enviroment; - private Dictionary envVariables; - - [SetUp] - public void SetUp() - { - envVariables = new Dictionary(); - - _fileSystem = Substitute.For(); - - _fileSystem.TryReadAllTextFromFile(Arg.Any()).Returns(a => @"{ - DC: 'il11', - ENV: 'orl11', - GIGYA_CONFIG_PATHS_FILE: 'C:\\gigya\\Config\\loadPaths1.json', - }"); - - enviroment = Substitute.For(); - enviroment.GetEnvironmentVariable(Arg.Any()).Returns(a => - { - envVariables.TryGetValue(a.Arg(), out string val); - return val; - }); - enviroment.When(a => a.SetEnvironmentVariableForProcess(Arg.Any(), Arg.Any())).Do(a => - { - envVariables[a.ArgAt(0)] = a.ArgAt(1); - }); - } - - [Test] - public void ReadsEnvFromDifferentFile() - { - envVariables = new Dictionary{ - {"GIGYA_ENVVARS_FILE", "C:\\gigya\\envVars.json"} - }; - - - var reader = new EnvironmentVariablesFileReader(_fileSystem, enviroment); - reader.ReadFromFile(); - - - _fileSystem.Received().TryReadAllTextFromFile("C:\\gigya\\envVars.json"); - } - - [Test] - public void ReadsEnvFromDefaultFile() - { - var envFilePath = enviroment.PlatformSpecificPathPrefix + "/gigya/environmentVariables.json"; - - var reader = new EnvironmentVariablesFileReader(_fileSystem, enviroment); - reader.ReadFromFile(); - - - _fileSystem.Received().TryReadAllTextFromFile(envFilePath); - } - - [Test] - public void ReadAndSeEnvVariables_SomeEmpty() - { - envVariables = new Dictionary {{"DC", "il2"}}; - - var reader = new EnvironmentVariablesFileReader(_fileSystem, enviroment); - reader.ReadFromFile(); - - enviroment.GetEnvironmentVariable("DC").ShouldBe("il11"); - enviroment.GetEnvironmentVariable("ENV").ShouldBe("orl11"); - enviroment.GetEnvironmentVariable("GIGYA_CONFIG_PATHS_FILE").ShouldBe("C:\\gigya\\Config\\loadPaths1.json"); - } - - - [Test] - public void ReadAndSeEnvVariables_AllEmpty() - { - - envVariables = new Dictionary(); - - var reader = new EnvironmentVariablesFileReader(_fileSystem, enviroment); - reader.ReadFromFile(); - - enviroment.GetEnvironmentVariable("DC").ShouldBe("il11"); - enviroment.GetEnvironmentVariable("ENV").ShouldBe("orl11"); - enviroment.GetEnvironmentVariable("GIGYA_CONFIG_PATHS_FILE").ShouldBe("C:\\gigya\\Config\\loadPaths1.json"); - } - - [Test] - public void OnNotExistingFile_DoNothing() - { - - _fileSystem.TryReadAllTextFromFile(Arg.Any()).Returns(a => null); - - var reader = new EnvironmentVariablesFileReader(_fileSystem, enviroment); - reader.ReadFromFile(); - - enviroment.DidNotReceiveWithAnyArgs().WhenForAnyArgs(a => a.SetEnvironmentVariableForProcess(Arg.Any(), Arg.Any())); - } - - - [Test] - public void OnFileParsingFailure_DoNothing() - { - _fileSystem.TryReadAllTextFromFile(Arg.Any()).Returns(a => @"Invalid JSON file"); - - Action doAction = () => - { - var reader = new EnvironmentVariablesFileReader(_fileSystem, enviroment); - reader.ReadFromFile(); - }; - doAction.ShouldThrow(); - enviroment.DidNotReceiveWithAnyArgs().WhenForAnyArgs(a => a.SetEnvironmentVariableForProcess(Arg.Any(), Arg.Any())); - } - } -} diff --git a/tests/Gigya.Microdot.UnitTests/Configuration/EnvironmentVariableProviderTests.cs b/tests/Gigya.Microdot.UnitTests/Configuration/EnvironmentVariableProviderTests.cs new file mode 100644 index 00000000..7ec8cd38 --- /dev/null +++ b/tests/Gigya.Microdot.UnitTests/Configuration/EnvironmentVariableProviderTests.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; + +using Gigya.Microdot.Configuration; +using Gigya.Microdot.Interfaces.Configuration; +using Gigya.Microdot.Interfaces.SystemWrappers; +using Gigya.Microdot.SharedLogic.Exceptions; + +using NSubstitute; + +using NUnit.Framework; + +using Shouldly; + +namespace Gigya.Microdot.UnitTests.Configuration +{ + public class EnvironmentVariableProviderTests + { + private const string DEFAULT_REGION = "default_region"; + private const string DEFAULT_ZONE = "default_zone"; + private const string DEFAULT_ENV = "default_env"; + + private IFileSystem _fileSystem; + private string _originalENV; + private string _originalREGION; + private string _originalZone; + + [SetUp] + public void SetUp() + { + _fileSystem = Substitute.For(); + + _fileSystem.TryReadAllTextFromFile(Arg.Any()).Returns(a => @"{ + REGION: 'il1', + ZONE: 'il1a', + ENV: 'orl11', + GIGYA_CONFIG_PATHS_FILE: 'C:\\gigya\\Config\\loadPaths1.json', + }"); + + _originalREGION = Environment.GetEnvironmentVariable("REGION"); + _originalZone = Environment.GetEnvironmentVariable("ZONE"); + _originalENV = Environment.GetEnvironmentVariable("ENV"); + Environment.SetEnvironmentVariable("ZONE", DEFAULT_ZONE); + Environment.SetEnvironmentVariable("REGION", DEFAULT_REGION); + Environment.SetEnvironmentVariable("ENV", DEFAULT_ENV); + Environment.SetEnvironmentVariable("GIGYA_ENVVARS_FILE", null); + Environment.SetEnvironmentVariable("GIGYA_CONFIG_PATHS_FILE", null); + } + + [TearDown] + public void TearDown() + { + Environment.SetEnvironmentVariable("GIGYA_ENVVARS_FILE", null); + Environment.SetEnvironmentVariable("GIGYA_CONFIG_PATHS_FILE", null); + Environment.SetEnvironmentVariable("ZONE", _originalZone); + Environment.SetEnvironmentVariable("REGION", _originalREGION); + Environment.SetEnvironmentVariable("ENV", _originalENV); + } + + [Test] + public void ReadsEnvFromDifferentFile() + { + Environment.SetEnvironmentVariable("GIGYA_ENVVARS_FILE", "C:\\gigya\\envVars.json"); + new EnvironmentVariableProvider(_fileSystem); + + _fileSystem.Received().TryReadAllTextFromFile("c:\\gigya\\envvars.json"); + } + + [Test] + public void ReadsEnvFromDefaultFile() + { + var environmentVariableProvider = new EnvironmentVariableProvider(_fileSystem); + + _fileSystem.Received().TryReadAllTextFromFile(environmentVariableProvider.PlatformSpecificPathPrefix + "/gigya/environmentVariables.json"); + } + + [Test] + public void ReadAndSeEnvVariables_SomeEmpty() + { + Environment.SetEnvironmentVariable("ZONE", "il1b"); + + var environmentVariableProvider = new EnvironmentVariableProvider(_fileSystem); + + environmentVariableProvider.GetEnvironmentVariable("ZONE").ShouldBe("il1a"); + environmentVariableProvider.GetEnvironmentVariable("REGION").ShouldBe("il1"); + environmentVariableProvider.GetEnvironmentVariable("ENV").ShouldBe("orl11"); + environmentVariableProvider.GetEnvironmentVariable("GIGYA_CONFIG_PATHS_FILE").ShouldBe("c:\\gigya\\config\\loadpaths1.json"); + } + + + [Test] + public void ReadAndSeEnvVariables_AllEmpty() + { + var environmentVariableProvider = new EnvironmentVariableProvider(_fileSystem); + + environmentVariableProvider.GetEnvironmentVariable("ZONE").ShouldBe("il1a"); + environmentVariableProvider.GetEnvironmentVariable("REGION").ShouldBe("il1"); + environmentVariableProvider.GetEnvironmentVariable("ENV").ShouldBe("orl11"); + environmentVariableProvider.GetEnvironmentVariable("GIGYA_CONFIG_PATHS_FILE").ShouldBe("c:\\gigya\\config\\loadpaths1.json"); + } + + [Test] + public void OnNotExistingFile_DoNothing() + { + _fileSystem.TryReadAllTextFromFile(Arg.Any()).Returns(a => null); + + var environmentVariableProvider = new EnvironmentVariableProvider(_fileSystem); + + // assert environment variables were not changed + environmentVariableProvider.GetEnvironmentVariable("ZONE").ShouldBe(DEFAULT_ZONE); + environmentVariableProvider.GetEnvironmentVariable("REGION").ShouldBe(DEFAULT_REGION); + environmentVariableProvider.GetEnvironmentVariable("ENV").ShouldBe(DEFAULT_ENV); + } + + + [Test] + public void OnFileParsingFailure_DoNothing() + { + _fileSystem.TryReadAllTextFromFile(Arg.Any()).Returns(a => @"Invalid JSON file"); + + Action doAction = () => + { + new EnvironmentVariableProvider(_fileSystem); + }; + doAction.ShouldThrow(); + } + } +} diff --git a/tests/Gigya.Microdot.UnitTests/Configuration/MasterConfigParserTests.cs b/tests/Gigya.Microdot.UnitTests/Configuration/MasterConfigParserTests.cs index 43cec46f..c6cf30f1 100644 --- a/tests/Gigya.Microdot.UnitTests/Configuration/MasterConfigParserTests.cs +++ b/tests/Gigya.Microdot.UnitTests/Configuration/MasterConfigParserTests.cs @@ -19,21 +19,20 @@ namespace Gigya.Microdot.UnitTests.Configuration public class MasterConfigParserTests { private IFileSystem _fileSystem; - private IEnvironment environment; private IEnvironmentVariableProvider environmentVariableProvider; private const string env = "env1"; - private const string dc = "dc1"; + private const string zone = "dc1"; private const string testData = @"//$(prefix) is a root folder c:\ or \etc [ {Pattern: '$(prefix)/Gigya/Config/*.config', Priority: 2, SearchOption: 'TopDirectoryOnly' }, {Pattern: '$(prefix)/Gigya/Config/$(appName)/*.config', Priority: 3, SearchOption: 'TopDirectoryOnly' }, {Pattern: '$(prefix)/Gigya/Config/%ENV%/*.config', Priority: 4, SearchOption: 'TopDirectoryOnly' }, - {Pattern: '$(prefix)/Gigya/Config/%DC%/*.config', Priority: 5, SearchOption: 'TopDirectoryOnly' }, - {Pattern: '$(prefix)/Gigya/Config/%DC%/$(appName)/*.config', Priority: 6, SearchOption: 'TopDirectoryOnly' }, - {Pattern: '$(prefix)/Gigya/Config/%DC%/%ENV%/*.config', Priority: 7, SearchOption: 'TopDirectoryOnly' }, - {Pattern: '$(prefix)/Gigya/Config/%DC%/%ENV%/$(appName)/*.config', Priority: 8, SearchOption: 'TopDirectoryOnly' }, + {Pattern: '$(prefix)/Gigya/Config/%ZONE%/*.config', Priority: 5, SearchOption: 'TopDirectoryOnly' }, + {Pattern: '$(prefix)/Gigya/Config/%ZONE%/$(appName)/*.config', Priority: 6, SearchOption: 'TopDirectoryOnly' }, + {Pattern: '$(prefix)/Gigya/Config/%ZONE%/%ENV%/*.config', Priority: 7, SearchOption: 'TopDirectoryOnly' }, + {Pattern: '$(prefix)/Gigya/Config/%ZONE%/%ENV%/$(appName)/*.config', Priority: 8, SearchOption: 'TopDirectoryOnly' }, {Pattern: '$(prefix)/Gigya/Config/_local/*.config', Priority: 9, SearchOption: 'TopDirectoryOnly' }, {Pattern: './Config/*.config', Priority: 10, SearchOption: 'AllDirectories' } ]"; @@ -46,10 +45,9 @@ public void SetUp() _fileSystem.ReadAllTextFromFile(Arg.Any()).Returns(a => testData); _fileSystem.Exists(Arg.Any()).Returns(a => true); - environment = Substitute.For(); - environment.PlatformSpecificPathPrefix.Returns("c:"); + environmentVariableProvider = Substitute.For(); + environmentVariableProvider.PlatformSpecificPathPrefix.Returns("c:"); - environmentVariableProvider = Substitute.For(); } @@ -59,10 +57,10 @@ public void AllPathExists_AllEnvironmentVariablesExists_EnvironmentExceptionExpe var expected = new[] { new ConfigFileDeclaration {Pattern = $"./Config/*.config", Priority = 10}, new ConfigFileDeclaration {Pattern = $"c:/Gigya/Config/_local/*.config", Priority = 9}, - new ConfigFileDeclaration {Pattern = $"c:/Gigya/Config/{dc}/{env}/{CurrentApplicationInfo.Name}/*.config", Priority = 8}, - new ConfigFileDeclaration {Pattern = $"c:/Gigya/Config/{dc}/{env}/*.config", Priority = 7}, - new ConfigFileDeclaration {Pattern = $"c:/Gigya/Config/{dc}/{CurrentApplicationInfo.Name}/*.config", Priority = 6}, - new ConfigFileDeclaration {Pattern = $"c:/Gigya/Config/{dc}/*.config", Priority = 5}, + new ConfigFileDeclaration {Pattern = $"c:/Gigya/Config/{zone}/{env}/{CurrentApplicationInfo.Name}/*.config", Priority = 8}, + new ConfigFileDeclaration {Pattern = $"c:/Gigya/Config/{zone}/{env}/*.config", Priority = 7}, + new ConfigFileDeclaration {Pattern = $"c:/Gigya/Config/{zone}/{CurrentApplicationInfo.Name}/*.config", Priority = 6}, + new ConfigFileDeclaration {Pattern = $"c:/Gigya/Config/{zone}/*.config", Priority = 5}, new ConfigFileDeclaration {Pattern = $"c:/Gigya/Config/{env}/*.config", Priority = 4}, new ConfigFileDeclaration {Pattern = $"c:/Gigya/Config/{CurrentApplicationInfo.Name}/*.config", Priority = 3}, new ConfigFileDeclaration {Pattern = $"c:/Gigya/Config/*.config", Priority = 2} @@ -71,7 +69,7 @@ public void AllPathExists_AllEnvironmentVariablesExists_EnvironmentExceptionExpe BaseTest(new Dictionary { {"ENV", env}, - {"DC", dc} + {"ZONE", zone} }, expected); } @@ -95,7 +93,7 @@ public void AllPathExists_NoEnvironmentVariablesExists_EnvironmentExceptionExpec public void FileFormatIsInvalid_ShouldThrowEnvironmentException(string testData) { _fileSystem.ReadAllTextFromFile(Arg.Any()).Returns(a => testData); - Action act = () => new ConfigurationLocationsParser(environment, _fileSystem, environmentVariableProvider); + Action act = () => new ConfigurationLocationsParser(_fileSystem, environmentVariableProvider); act.ShouldThrow() .Message.ShouldContain("Problem reading"); @@ -110,7 +108,7 @@ public void DuplicatePriority_ShouldThrowEnvironmentException() {Pattern: '$(prefix)/Gigya/Config/$(appName)/*.config', Priority: 1, SearchOption: 'TopDirectoryOnly' }]"; _fileSystem.ReadAllTextFromFile(Arg.Any()).Returns(a => testData); - Action act = () => new ConfigurationLocationsParser(environment, _fileSystem, environmentVariableProvider); + Action act = () => new ConfigurationLocationsParser(_fileSystem, environmentVariableProvider); act.ShouldThrow() .Message.ShouldContain("some configurations lines have duplicate priorities"); @@ -124,7 +122,7 @@ public void BaseTest(Dictionary envDictionary, ConfigFileDeclar return val; }); - var configs = new ConfigurationLocationsParser(environment, _fileSystem, environmentVariableProvider); + var configs = new ConfigurationLocationsParser(_fileSystem, environmentVariableProvider); configs.ConfigFileDeclarations.Count.ShouldBe(expected.Length); foreach (var pair in configs.ConfigFileDeclarations.Zip(expected, (first, second) => new { first, second })) diff --git a/tests/Gigya.Microdot.UnitTests/Discovery/ConfigNodeSourceTests.cs b/tests/Gigya.Microdot.UnitTests/Discovery/ConfigNodeSourceTests.cs new file mode 100644 index 00000000..76dd97c2 --- /dev/null +++ b/tests/Gigya.Microdot.UnitTests/Discovery/ConfigNodeSourceTests.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Gigya.Common.Contracts.Exceptions; +using Gigya.Microdot.Fakes; +using Gigya.Microdot.Interfaces.SystemWrappers; +using Gigya.Microdot.ServiceDiscovery; +using Gigya.Microdot.ServiceDiscovery.Config; +using Gigya.Microdot.ServiceDiscovery.Rewrite; +using Gigya.Microdot.SharedLogic.Exceptions; +using Gigya.Microdot.SharedLogic.Rewrite; +using Gigya.Microdot.Testing.Shared; +using Ninject; +using NSubstitute; +using NUnit.Framework; +using Shouldly; + + +namespace Gigya.Microdot.UnitTests.Discovery +{ + public class ConfigNodeSourceTests + { + private const string ServiceName = "MyService"; + + private TestingKernel _kernel; + private INodeSource _configNodeSource; + private DiscoveryConfig _discoveryConfig; + + [OneTimeSetUp] + public void OneTimeSetup() + { + _kernel = new TestingKernel(); + _kernel.Rebind>().ToMethod(c => () => _discoveryConfig); + } + + [OneTimeTearDown] + public void OneTimeTearDown() + { + _kernel.Dispose(); + } + + [SetUp] + public void Setup() + { + var environment = Substitute.For(); + var deployment = new DeploymentIdentifier(ServiceName, "prod", environment); + + _configNodeSource = _kernel.Get>()(deployment); + } + + private async Task SetConfigHosts(string hosts) + { + _discoveryConfig = new DiscoveryConfig {Services = new ServiceDiscoveryCollection(new Dictionary(), new ServiceDiscoveryConfig(), new PortAllocationConfig()) }; + if (hosts != null) + _discoveryConfig.Services[ServiceName].Hosts = hosts; + } + + [Test] + public void ThrowExceptionIfConfigIsEmpty() + { + SetConfigHosts(null); + var getNodes = (Action)(() => GetNodes()); + getNodes.ShouldThrow(); + } + + [Test] + public void ConfigWithOneHost() + { + SetConfigHosts("myHost"); + var nodes = GetNodes(); + nodes[0].Hostname.ShouldBe("myHost"); + nodes[0].Port.ShouldBe(null); + } + + [Test] + public void ConfigWithOneHostAndPort() + { + SetConfigHosts("myHost:123"); + var nodes = GetNodes(); + nodes[0].Hostname.ShouldBe("myHost"); + nodes[0].Port.ShouldBe(123); + } + + [Test] + public void ConfigWithMoreThanOneHost() + { + SetConfigHosts("host1,host2:333"); + var nodes = GetNodes(); + nodes[0].Hostname.ShouldBe("host1"); + nodes[0].Port.ShouldBe(null); + nodes[1].Hostname.ShouldBe("host2"); + nodes[1].Port.ShouldBe(333); + } + + [Test] + public void ConfigWithEmptySpaces() + { + SetConfigHosts("host1, host2,,,host3"); + var nodes = GetNodes(); + nodes[0].Hostname.ShouldBe("host1"); + nodes[1].Hostname.ShouldBe("host2"); + nodes[2].Hostname.ShouldBe("host3"); + } + + [Test] + public void ConfigWithIncorrectPortDefinition() + { + SetConfigHosts("myHost:1:2:3"); + ShouldThrowExtensions.ShouldThrow(()=>GetNodes(), typeof(ConfigurationException)); + } + + [Test] + public void UpdateNodesWhenConfigurationUpdated() + { + SetConfigHosts("host1"); + var nodes = GetNodes(); + nodes[0].Hostname.ShouldBe("host1"); + + SetConfigHosts("host2"); + nodes = GetNodes(); + nodes[0].Hostname.ShouldBe("host2"); + + } + + private Node[] GetNodes() + { + return _configNodeSource.GetNodes(); + } + } +} + diff --git a/tests/Gigya.Microdot.UnitTests/Discovery/ConsulClientTests.cs b/tests/Gigya.Microdot.UnitTests/Discovery/ConsulClientTests.cs index 3c92d0df..5b5281fe 100644 --- a/tests/Gigya.Microdot.UnitTests/Discovery/ConsulClientTests.cs +++ b/tests/Gigya.Microdot.UnitTests/Discovery/ConsulClientTests.cs @@ -21,7 +21,7 @@ public class ConsulClientTests { private const string ServiceName = "MyService-prod"; private const int ConsulPort = 8501; - private const string DataCenter = "us1"; + private const string Zone = "us1a"; private const string Host1 = "Host1"; private const int Port1 = 1234; @@ -31,7 +31,7 @@ public enum ConsulMethod { LongPolling, Queries} private TestingKernel _testingKernel; private IConsulClient _consulClient; - private IEnvironmentVariableProvider _environmentVariableProvider; + private IEnvironment _environment; private ConsulSimulator _consulSimulator; private string _serviceName; private DateTimeFake _dateTimeFake; @@ -44,10 +44,10 @@ public void SetupConsulListener() _testingKernel = new TestingKernel(k => { - _environmentVariableProvider = Substitute.For(); - _environmentVariableProvider.ConsulAddress.Returns($"{CurrentApplicationInfo.HostName}:{ConsulPort}"); - _environmentVariableProvider.DataCenter.Returns(DataCenter); - k.Rebind().ToMethod(_ => _environmentVariableProvider); + _environment = Substitute.For(); + _environment.ConsulAddress.Returns($"{CurrentApplicationInfo.HostName}:{ConsulPort}"); + _environment.Zone.Returns(Zone); + k.Rebind().ToMethod(_ => _environment); k.Rebind().ToMethod(_ => _dateTimeFake); @@ -59,18 +59,17 @@ public void SetupConsulListener() [OneTimeTearDown] public void TearDownConsulListener() { - _consulSimulator.Dispose(); _testingKernel.Dispose(); + _consulSimulator.Dispose(); } [SetUp] public void Setup() { + _consulSimulator.Reset(); _serviceName = ServiceName + "_" + Guid.NewGuid(); _dateTimeFake = new DateTimeFake(false); _consulConfig = new ConsulConfig(); - - _consulSimulator.Reset(); } private Task Start(ConsulMethod consulMethod) @@ -91,16 +90,13 @@ public async Task EndpointExists(ConsulMethod consulMethod) AssertOneDefaultEndpoint(result); } - [Test] + [Test] public async Task EndpointAdded_LongPolling() { await Start(ConsulMethod.LongPolling); var result = await GetResultAfter(() => AddServiceEndPoint()); AssertOneDefaultEndpoint(result); - var delays = _dateTimeFake.DelaysRequested.ToArray(); - delays.Length.ShouldBeLessThanOrEqualTo(4); // one kv call (find that service not deployed), one all-keys call (when service not deployed), one more kv call (after endpoint added), and one health call (to get endpoints) - delays.ShouldAllBe(d=>d.TotalSeconds==0); // don't wait between calls } [Test] @@ -248,12 +244,12 @@ private static void AssertOneDefaultEndpoint(EndPointsResult result) private async void AddServiceEndPoint(string hostName=Host1, int port=Port1, string version=Version, string serviceName=null) { - _consulSimulator.AddServiceEndpoint(serviceName??_serviceName, new ConsulEndPoint {HostName = hostName, Port = port, Version = version}); + _consulSimulator.AddServiceNode(serviceName??_serviceName, new ConsulEndPoint {HostName = hostName, Port = port, Version = version}); } private async void RemoveServiceEndPoint(string hostName = Host1, int port = Port1, string serviceName=null) { - _consulSimulator.RemoveServiceEndpoint(serviceName??_serviceName, new ConsulEndPoint { HostName = hostName, Port = port}); + _consulSimulator.RemoveServiceNode(serviceName??_serviceName, new ConsulEndPoint { HostName = hostName, Port = port}); } private void SetServiceVersion(string version, string serviceName=null) diff --git a/tests/Gigya.Microdot.UnitTests/Discovery/ConsulDiscoveryMasterFallBackTest.cs b/tests/Gigya.Microdot.UnitTests/Discovery/ConsulDiscoveryMasterFallBackTest.cs index 1a23ea8c..bb288372 100644 --- a/tests/Gigya.Microdot.UnitTests/Discovery/ConsulDiscoveryMasterFallBackTest.cs +++ b/tests/Gigya.Microdot.UnitTests/Discovery/ConsulDiscoveryMasterFallBackTest.cs @@ -32,7 +32,7 @@ public class ConsulDiscoveryMasterFallBackTest private Dictionary _configDic; private TestingKernel _unitTestingKernel; private Dictionary _consulClient; - private IEnvironmentVariableProvider _environmentVariableProvider; + private IEnvironment _environment; private ManualConfigurationEvents _configRefresh; private IDateTime _dateTimeMock; private int id; @@ -44,14 +44,14 @@ public void SetUp() _unitTestingKernel?.Dispose(); _serviceName = $"ServiceName{++id}"; - _environmentVariableProvider = Substitute.For(); - _environmentVariableProvider.DataCenter.Returns("il3"); - _environmentVariableProvider.DeploymentEnvironment.Returns(ORIGINATING_ENVIRONMENT); + _environment = Substitute.For(); + _environment.Zone.Returns("il3"); + _environment.DeploymentEnvironment.Returns(ORIGINATING_ENVIRONMENT); _configDic = new Dictionary {{"Discovery.EnvironmentFallbackEnabled", "true"}}; _unitTestingKernel = new TestingKernel(k => { - k.Rebind().ToConstant(_environmentVariableProvider); + k.Rebind().ToConstant(_environment); k.Rebind().To().InSingletonScope(); SetupConsulClientMocks(); @@ -63,8 +63,8 @@ public void SetUp() }, _configDic); _configRefresh = _unitTestingKernel.Get(); - var environmentVariableProvider = _unitTestingKernel.Get(); - Assert.AreEqual(_environmentVariableProvider, environmentVariableProvider); + var environment = _unitTestingKernel.Get(); + Assert.AreEqual(_environment, environment); } private void SetupConsulClientMocks() @@ -143,9 +143,9 @@ public async Task CreateServiceDiscoveyWithoutGetNextHostNoServiceHealthShouldAp [Test] [Repeat(Repeat)] - public async Task ScopeDataCenterShouldUseServiceNameAsConsoleQuery() + public async Task ScopeZoneShouldUseServiceNameAsConsoleQuery() { - _configDic[$"Discovery.Services.{_serviceName}.Scope"] = "DataCenter"; + _configDic[$"Discovery.Services.{_serviceName}.Scope"] = "Zone"; SetMockToReturnHost(_serviceName); var nextHost = GetServiceDiscovey().GetNextHost(); (await nextHost).HostName.ShouldBe(_serviceName); @@ -226,10 +226,10 @@ public async Task QueryDefinedShouldNotFallBackToMaster() [Repeat(Repeat)] public void MasterShouldNotFallBack() { - _environmentVariableProvider = Substitute.For(); - _environmentVariableProvider.DataCenter.Returns("il3"); - _environmentVariableProvider.DeploymentEnvironment.Returns(MASTER_ENVIRONMENT); - _unitTestingKernel.Rebind().ToConstant(_environmentVariableProvider); + _environment = Substitute.For(); + _environment.Zone.Returns("il3"); + _environment.DeploymentEnvironment.Returns(MASTER_ENVIRONMENT); + _unitTestingKernel.Rebind().ToConstant(_environment); SetMockToReturnServiceNotDefined(MasterService); diff --git a/tests/Gigya.Microdot.UnitTests/Discovery/ConsulDiscoverySourceTest.cs b/tests/Gigya.Microdot.UnitTests/Discovery/ConsulDiscoverySourceTest.cs index 9a8caf40..688266ea 100644 --- a/tests/Gigya.Microdot.UnitTests/Discovery/ConsulDiscoverySourceTest.cs +++ b/tests/Gigya.Microdot.UnitTests/Discovery/ConsulDiscoverySourceTest.cs @@ -44,16 +44,17 @@ public class ConsulDiscoverySourceTest private Dictionary _configDic; private Func _consulClientInitTask; private DateTimeFake _dateTimeFake; + private IEnvironment _environmentMock; [SetUp] public void Setup() { _configDic = new Dictionary(); - _unitTestingKernel = new TestingKernel(k=>k.Rebind().To(), _configDic); + _unitTestingKernel = new TestingKernel(k => {}, _configDic); - var environmentVarialbesMock = Substitute.For(); - environmentVarialbesMock.DeploymentEnvironment.Returns(ENV); - Kernel.Rebind().ToConstant(environmentVarialbesMock); + _environmentMock = Substitute.For(); + _environmentMock.DeploymentEnvironment.Returns(ENV); + Kernel.Rebind().ToConstant(_environmentMock); SetupDateTimeFake(); SetupConsulClient(); @@ -118,9 +119,9 @@ public async Task ServiceInEnvironmentScope() } [Test] - public async Task ServiceInDataCenterScope() + public async Task ServiceInZoneScope() { - _configDic[$"Discovery.Services.{SERVICE_NAME}.Scope"] = "DataCenter"; + _configDic[$"Discovery.Services.{SERVICE_NAME}.Scope"] = "Zone"; await GetFirstResult().ConfigureAwait(false); Assert.AreEqual($"{SERVICE_NAME}", _requestedConsulServiceName); } @@ -131,8 +132,8 @@ private async Task GetFirstResult() { Scope = _serviceScope, }; - var sourceFactory = Kernel.Get>(); - var serviceContext = new ServiceDeployment(SERVICE_NAME, ENV); + var sourceFactory = Kernel.Get>(); + var serviceContext = new DeploymentIdentifier(SERVICE_NAME, ENV, _environmentMock); _consulDiscoverySource = sourceFactory(serviceContext, config); await _consulDiscoverySource.Init(); await GetNewResult(); diff --git a/tests/Gigya.Microdot.UnitTests/Discovery/ConsulSimulator.cs b/tests/Gigya.Microdot.UnitTests/Discovery/ConsulSimulator.cs index 4334189a..3cfd0d3b 100644 --- a/tests/Gigya.Microdot.UnitTests/Discovery/ConsulSimulator.cs +++ b/tests/Gigya.Microdot.UnitTests/Discovery/ConsulSimulator.cs @@ -5,6 +5,7 @@ using System.Net; using System.Text; using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; using Gigya.Microdot.ServiceDiscovery; using Gigya.Microdot.SharedLogic; @@ -26,6 +27,12 @@ public sealed class ConsulSimulator: IDisposable private TaskCompletionSource _waitForHealthIndexModification; private Exception _httpErrorFake; + private int _requestsCounter = 0; + private int _healthRequestsCounter = 0; + private int _queryRequestsCounter = 0; + private int _allKeysRequestsCounter = 0; + private int _keyValueReuqestsCounter = 0; + public ConsulSimulator(int consulPort) { Reset(); @@ -57,10 +64,23 @@ public void Reset() _waitForHealthIndexModification = new TaskCompletionSource(); _waitForKeyValueIndexModification = new TaskCompletionSource(); _httpErrorFake = null; + _requestsCounter = 0; + _allKeysRequestsCounter = 0; + _keyValueReuqestsCounter = 0; + _healthRequestsCounter = 0; + _queryRequestsCounter = 0; } + public int RequestsCounter => _requestsCounter; + public int HealthRequestsCounter => _healthRequestsCounter; + public int QueryRequestsCounter => _queryRequestsCounter; + public int AllKeysRequestsCounter => _allKeysRequestsCounter; + public int KeyValueReuqestsCounter => _keyValueReuqestsCounter; + private async Task GetHealthResponse(string serviceName, ulong index) { + Interlocked.Increment(ref _healthRequestsCounter); + if (index >= _healthModifyIndex) await _waitForHealthIndexModification.Task; @@ -84,6 +104,8 @@ private async Task GetHealthResponse(string serviceName, ulong i private async Task GetKeyValueResponse(string serviceName, ulong index) { + Interlocked.Increment(ref _keyValueReuqestsCounter); + if (index >= _keyValueModifyIndex) await _waitForKeyValueIndexModification.Task; @@ -102,6 +124,8 @@ private async Task GetKeyValueResponse(string serviceName, ulong private async Task GetAllKeysResponse(string serviceName, ulong index) { + Interlocked.Increment(ref _allKeysRequestsCounter); + if (index >= _keyValueModifyIndex) await _waitForKeyValueIndexModification.Task; @@ -115,6 +139,8 @@ private async Task GetAllKeysResponse(string serviceName, ulong private async Task GetQueryResponse(string serviceName, ulong index) { + Interlocked.Increment(ref _queryRequestsCounter); + if (!_serviceNodes.ContainsKey(serviceName) && !_serviceActiveVersion.ContainsKey(serviceName)) return new ConsulResponse { @@ -142,9 +168,10 @@ private async Task GetQueryResponse(string serviceName, ulong in }; } - public void AddServiceEndpoint(string serviceName, ConsulEndPoint endPoint) + public void AddServiceNode(string serviceName, ConsulEndPoint endPoint) { - _serviceNodes.TryAdd(serviceName, new List()); + if (_serviceNodes.TryAdd(serviceName, new List())) + IncreaseKeyValueModifyIndex(); _serviceNodes[serviceName].Add(endPoint); if (endPoint.Version!=null && !_serviceActiveVersion.TryGetValue(serviceName, out string _)) @@ -153,7 +180,7 @@ public void AddServiceEndpoint(string serviceName, ConsulEndPoint endPoint) IncreaseHealthModifyIndex(); } - public void RemoveServiceEndpoint(string serviceName, ConsulEndPoint endPoint) + public void RemoveServiceNode(string serviceName, ConsulEndPoint endPoint) { var index = _serviceNodes[serviceName].FindIndex(ep => ep.HostName==endPoint.HostName && ep.Port==endPoint.Port); if (index < 0) @@ -165,6 +192,7 @@ public void RemoveServiceEndpoint(string serviceName, ConsulEndPoint endPoint) public void SetServiceVersion(string serviceName, string version) { + _serviceNodes.TryAdd(serviceName, new List()); _serviceActiveVersion.AddOrUpdate(serviceName, _ => version, (_,__)=> version); IncreaseKeyValueModifyIndex(); } @@ -180,6 +208,8 @@ public void RemoveService(string serviceName) public void SetError(Exception error) { _httpErrorFake = error; + IncreaseKeyValueModifyIndex(); + IncreaseHealthModifyIndex(); } private void IncreaseHealthModifyIndex() @@ -211,11 +241,17 @@ private async void StartListening() { break; } + catch (HttpListenerException) + { + break; + } } } private async void HandleConsulRequest(HttpListenerContext context) { + Interlocked.Increment(ref _requestsCounter); + var urlPath = context.Request.Url.PathAndQuery; if (_httpErrorFake == null) diff --git a/tests/Gigya.Microdot.UnitTests/Discovery/LocalNodeSourceTests.cs b/tests/Gigya.Microdot.UnitTests/Discovery/LocalNodeSourceTests.cs new file mode 100644 index 00000000..cb1adf1c --- /dev/null +++ b/tests/Gigya.Microdot.UnitTests/Discovery/LocalNodeSourceTests.cs @@ -0,0 +1,51 @@ +using System; +using System.Linq; +using System.Net; +using Gigya.Microdot.Fakes; +using Gigya.Microdot.Interfaces.SystemWrappers; +using Gigya.Microdot.ServiceDiscovery; +using Gigya.Microdot.ServiceDiscovery.Rewrite; +using Gigya.Microdot.SharedLogic.Rewrite; +using Gigya.Microdot.Testing.Shared; +using Ninject; +using NSubstitute; +using NUnit.Framework; +using Shouldly; + +namespace Gigya.Microdot.UnitTests.Discovery +{ + public class LocalNodeSourceTests + { + private TestingKernel _kernel; + private INodeSource _localNodeSource; + + [OneTimeSetUp] + public void OneTimeSetup() + { + _kernel = new TestingKernel(); + } + + [OneTimeTearDown] + public void OneTimeTearDown() + { + _kernel.Dispose(); + } + + [SetUp] + public void Setup() + { + var deployment = new DeploymentIdentifier("MyService", "prod", Substitute.For()); + + _localNodeSource = _kernel.Get>()(deployment); + } + + [Test] + public void OneSingleLocalHostNode() + { + var node = _localNodeSource.GetNodes().Single(); + node.Hostname.ShouldBe(Dns.GetHostName()); + node.Port.ShouldBeNull(); + } + + } +} diff --git a/tests/Gigya.Microdot.UnitTests/Discovery/RemoteHostPoolTests.cs b/tests/Gigya.Microdot.UnitTests/Discovery/RemoteHostPoolTests.cs index 1ab32ba5..71581473 100644 --- a/tests/Gigya.Microdot.UnitTests/Discovery/RemoteHostPoolTests.cs +++ b/tests/Gigya.Microdot.UnitTests/Discovery/RemoteHostPoolTests.cs @@ -9,6 +9,7 @@ using Gigya.Common.Contracts.Exceptions; using Gigya.Microdot.Fakes; using Gigya.Microdot.Interfaces.Logging; +using Gigya.Microdot.Interfaces.SystemWrappers; using Gigya.Microdot.ServiceDiscovery; using Gigya.Microdot.ServiceDiscovery.HostManagement; using Gigya.Microdot.SharedLogic.Monitor; @@ -18,7 +19,7 @@ using Metrics; using Ninject; - +using NSubstitute; using NUnit.Framework; using Shouldly; @@ -54,7 +55,7 @@ private void CreatePool(string endPoints, ReachabilityChecker isReachableChecker _discoverySourceMock = new DiscoverySourceMock(serviceContext, endPoints); Pool = factory.Create( - new ServiceDeployment(SERVICE_NAME, "prod"), + new DeploymentIdentifier(SERVICE_NAME, "prod", Substitute.For()), _discoverySourceMock, isReachableChecker ?? (rh => Task.FromResult(false))); diff --git a/tests/Gigya.Microdot.UnitTests/Discovery/Rewrite/ConsulNodeSourceFactoryTests.cs b/tests/Gigya.Microdot.UnitTests/Discovery/Rewrite/ConsulNodeSourceFactoryTests.cs new file mode 100644 index 00000000..1b738112 --- /dev/null +++ b/tests/Gigya.Microdot.UnitTests/Discovery/Rewrite/ConsulNodeSourceFactoryTests.cs @@ -0,0 +1,221 @@ +using System; +using System.Threading.Tasks; +using Gigya.Common.Contracts.Exceptions; +using Gigya.Microdot.Fakes; +using Gigya.Microdot.Interfaces.Configuration; +using Gigya.Microdot.Interfaces.SystemWrappers; +using Gigya.Microdot.ServiceDiscovery; +using Gigya.Microdot.ServiceDiscovery.Config; +using Gigya.Microdot.ServiceDiscovery.Rewrite; +using Gigya.Microdot.SharedLogic; +using Gigya.Microdot.SharedLogic.Monitor; +using Gigya.Microdot.Testing.Shared; +using Metrics; +using Ninject; +using NSubstitute; +using NUnit.Framework; +using Shouldly; + +namespace Gigya.Microdot.UnitTests.Discovery.Rewrite +{ + [TestFixture] + public class ConsulNodeSourceFactoryTests + { + private const string ServiceName = "MyService"; + private const string Env = "prod"; + private const int ConsulPort = 8502; + private const string Zone = "us1a"; + private const string Host1 = "Host1"; + private const int Port1 = 1234; + private const string Version = "1.0.0.1"; + + private DeploymentIdentifier _deploymentIdentifier; + + private TestingKernel _testingKernel; + private ConsulConfig _consulConfig; + private ConsulSimulator _consulSimulator; + private IEnvironment _environment; + private ConsulNodeSourceFactory _factory; + + [OneTimeSetUp] + public void SetupConsulListener() + { + _consulSimulator = new ConsulSimulator(ConsulPort); + _testingKernel = new TestingKernel(k => + { + _environment = Substitute.For(); + _environment.ConsulAddress.Returns($"{CurrentApplicationInfo.HostName}:{ConsulPort}"); + _environment.Zone.Returns(Zone); + k.Rebind().ToMethod(_ => _environment); + + k.Rebind>().ToMethod(_ => () => _consulConfig); + }); + } + + [OneTimeTearDown] + public void TearDownConsulListener() + { + _consulSimulator.Dispose(); + _testingKernel.Dispose(); + } + + [SetUp] + public void Setup() + { + _consulSimulator.Reset(); + _deploymentIdentifier = new DeploymentIdentifier(ServiceName + "_" + Guid.NewGuid(), Env, _environment); + + _consulConfig = new ConsulConfig(); + } + + + [TearDown] + public void TearDown() + { + _factory?.Dispose(); + } + + + [Test] + public async Task ServiceMissingOnStart() + { + await Start(); + (await _factory.IsServiceDeployed(_deploymentIdentifier)).ShouldBeFalse(); + var nodeSource = await _factory.CreateNodeSource(_deploymentIdentifier); + nodeSource.ShouldBeNull(); + } + + [Test] + public async Task ServiceBecomesMissing() + { + AddService(); + await Start(); + + await ShouldCreateNodeSource(); + + RemoveService(); + await Task.Delay(800); + + await NodeSourceCannotBeCreated(); + } + + [Test] + public async Task ServiceAddedWhileRunning() + { + await Start(); + + await NodeSourceCannotBeCreated(); + + AddService(); + + await Task.Delay(800); + + await ShouldCreateNodeSource(); + } + + [Test] + public async Task ServiceWithNoNodes() + { + SetServiceVersion(); + await Start(); + await ShouldCreateNodeSource(); + } + + [Test] + public async Task ErrorOnStart() + { + await Start(); + SetError(); + await Task.Delay(800); + Should.Throw(async () => + { + var nodeSource = await _factory.CreateNodeSource(_deploymentIdentifier); + nodeSource.GetNodes(); + }); + GetHealthStatus().IsHealthy.ShouldBeFalse(); + } + + [Test] + public async Task ConsulResponsiveAfterError() + { + _consulConfig.ErrorRetryInterval = TimeSpan.FromMilliseconds(10); + SetError(); + await Start(); + _consulSimulator.Reset(); + AddService(); + await Task.Delay(800); + await ShouldCreateNodeSource(); + } + + [Test] + public async Task ErrorWhileRunning_ServiceStillAppearsOnList() + { + AddService(); + await Start(); + SetError(); + await Task.Delay(800); + await ShouldCreateNodeSource(); + GetHealthStatus().IsHealthy.ShouldBeFalse(); + } + + [Test] + public async Task ServiceListShouldAppearOnHealthCheck() + { + AddService(deploymentIdentifier: new DeploymentIdentifier("Service1", Env, _environment)); + AddService(deploymentIdentifier: new DeploymentIdentifier("Service2", Env, _environment)); + await Start(); + + var healthStatus = GetHealthStatus(); + healthStatus.IsHealthy.ShouldBeTrue(); + + healthStatus.Message.ShouldContain("Service1"); + healthStatus.Message.ShouldContain("Service2"); + } + + private async Task Start() + { + _factory = _testingKernel.Get(); + // try get some NodeSource in order to start init + try { await _factory.CreateNodeSource(null);} catch { } + } + + private void SetError() + { + _consulSimulator.SetError(new Exception("Fake Error on Consul")); + } + + private async void AddService(string hostName = Host1, int port = Port1, string version = Version, DeploymentIdentifier deploymentIdentifier = null) + { + _consulSimulator.AddServiceNode(deploymentIdentifier?.GetConsulServiceName() ?? _deploymentIdentifier.GetConsulServiceName(), new ConsulEndPoint { HostName = hostName, Port = port, Version = version }); + } + + private void RemoveService() + { + _consulSimulator.RemoveService(_deploymentIdentifier.GetConsulServiceName()); + } + + private void SetServiceVersion(string version=Version) + { + _consulSimulator.SetServiceVersion(_deploymentIdentifier.GetConsulServiceName(), version); + } + + private async Task ShouldCreateNodeSource(DeploymentIdentifier expectedDeploymentIdentifier=null) + { + (await _factory.IsServiceDeployed(_deploymentIdentifier)).ShouldBeTrue(); + _factory.CreateNodeSource(_deploymentIdentifier??expectedDeploymentIdentifier).Result.ShouldNotBeNull(); + } + + private async Task NodeSourceCannotBeCreated() + { + (await _factory.IsServiceDeployed(_deploymentIdentifier)).ShouldBeFalse(); + _factory.CreateNodeSource(_deploymentIdentifier).Result.ShouldBeNull(); + } + + private HealthCheckResult GetHealthStatus() + { + var healthMonitor = (FakeHealthMonitor)_testingKernel.Get(); + return healthMonitor.Monitors["ConsulServiceList"].Invoke(); + } + + } +} diff --git a/tests/Gigya.Microdot.UnitTests/Discovery/Rewrite/ConsulNodeSourceTests.cs b/tests/Gigya.Microdot.UnitTests/Discovery/Rewrite/ConsulNodeSourceTests.cs new file mode 100644 index 00000000..025c9227 --- /dev/null +++ b/tests/Gigya.Microdot.UnitTests/Discovery/Rewrite/ConsulNodeSourceTests.cs @@ -0,0 +1,285 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Gigya.Common.Contracts.Exceptions; +using Gigya.Microdot.Fakes; +using Gigya.Microdot.Interfaces.Configuration; +using Gigya.Microdot.Interfaces.SystemWrappers; +using Gigya.Microdot.ServiceDiscovery; +using Gigya.Microdot.ServiceDiscovery.Config; +using Gigya.Microdot.ServiceDiscovery.Rewrite; +using Gigya.Microdot.SharedLogic; +using Gigya.Microdot.SharedLogic.Monitor; +using Gigya.Microdot.SharedLogic.Rewrite; +using Gigya.Microdot.Testing.Shared; +using Metrics; +using Ninject; +using NSubstitute; +using NUnit.Framework; +using Shouldly; + +namespace Gigya.Microdot.UnitTests.Discovery.Rewrite +{ + [TestFixture] + public class ConsulNodeSourceTests + { + private const int ConsulPort = 8506; + private const string Zone = "us1a"; + + private const string Host1 = "Host1"; + private const int Port1 = 1234; + private const string Version = "1.0.0.1"; + + private const string Host2 = "Host2"; + private const int Port2 = 5678; + private const string Version2 = "2.0.0.1"; + + private TestingKernel _testingKernel; + private INodeSource _nodeSource; + private IEnvironment _environment; + private ConsulSimulator _consulSimulator; + private DeploymentIdentifier _deploymentIdentifier; + private ConsulConfig _consulConfig; + + private string _serviceName; + private ConsulNodeSourceFactory _consulNodeSourceFactory; + + [OneTimeSetUp] + public void OneTimeSetup() + { + _consulSimulator = new ConsulSimulator(ConsulPort); + } + + [OneTimeTearDown] + public void OneTimeTearDown() + { + _consulSimulator.Dispose(); + _testingKernel.Dispose(); + } + + [SetUp] + public async Task Setup() + { + _consulSimulator.Reset(); + _testingKernel = new TestingKernel(k => + { + _environment = Substitute.For(); + _environment.ConsulAddress.Returns($"{CurrentApplicationInfo.HostName}:{ConsulPort}"); + _environment.Zone.Returns(Zone); + k.Rebind().ToMethod(_ => _environment); + k.Rebind>().ToMethod(_ => () => _consulConfig); + k.Rebind().ToSelf().InTransientScope(); + }); + _serviceName = $"MyService_{Guid.NewGuid().ToString().Substring(5)}"; + + _deploymentIdentifier = new DeploymentIdentifier(_serviceName, "prod", Substitute.For()); + _consulConfig = new ConsulConfig {ErrorRetryInterval = TimeSpan.FromMilliseconds(10)}; + _consulNodeSourceFactory = _testingKernel.Get(); + } + + [TearDown] + public async Task Teardown() + { + _nodeSource?.Dispose(); + _testingKernel?.Dispose(); + _consulNodeSourceFactory?.Dispose(); + } + + public async Task WaitForUpdates() + { + await Task.Delay(1500).ConfigureAwait(false); + } + + [Test] + public async Task ServiceExists() + { + AddServiceNode(); + await Init(); + + await AssertOneDefaultNode(); + GetHealthStatus().IsHealthy.ShouldBeTrue(); + } + + [Test] + public async Task ServiceNotExists() + { + await Init(); + _nodeSource.ShouldBeNull(); + } + + [Test] + public async Task NodeAdded() + { + SetServiceVersion(Version); + AddServiceNode(); + await Init(); + + await AssertOneDefaultNode(); + + AddServiceNode(Host2); + await WaitForUpdates(); + var nodes = _nodeSource.GetNodes(); + nodes.Length.ShouldBe(2); + nodes[1].Hostname.ShouldBe(Host2); + } + + + [Test] + public async Task NodeRemoved() + { + AddServiceNode(); + AddServiceNode("nodeToRemove"); + + await Init(); + + _nodeSource.GetNodes().Length.ShouldBe(2); + + RemoveServiceEndPoint("nodeToRemove"); + await WaitForUpdates(); + + await AssertOneDefaultNode(); + } + + + [Test] + public async Task ServiceVersionHasChanged() + { + AddServiceNode(); + AddServiceNode(Host2, Port2, Version2); + await Init(); + await AssertOneDefaultNode(); + + SetServiceVersion(Version2); + await WaitForUpdates(); + + var nodes = _nodeSource.GetNodes(); + nodes.Length.ShouldBe(1); + nodes[0].Hostname.ShouldBe(Host2); + nodes[0].Port.ShouldBe(Port2); + GetHealthStatus().IsHealthy.ShouldBeTrue(); + } + + [Test] + public async Task ErrorWhileRunning_KeepLastKnownResult() + { + AddServiceNode(); + await Init(); + + SetConsulIsDown(); + await WaitForUpdates(); + + await AssertOneDefaultNode(); + + GetHealthStatus().IsHealthy.ShouldBeFalse(); + } + + + [Test] + public async Task ErrorOnStart() + { + SetConsulIsDown(); + Init().ShouldThrow(); + } + + [Test] + public async Task UpgradeVersion() + { + AddServiceNode(hostName: "oldVersionHost", version: "1.0.0"); + AddServiceNode(hostName: "newVersionHost", version: "2.0.0"); + SetServiceVersion("1.0.0"); + + await Init(); + + var nodes = _nodeSource.GetNodes(); + nodes.Length.ShouldBe(1); + nodes[0].Hostname.ShouldBe("oldVersionHost"); + + SetServiceVersion("2.0.0"); + await WaitForUpdates(); + + nodes = _nodeSource.GetNodes(); + nodes.Length.ShouldBe(1); + nodes[0].Hostname.ShouldBe("newVersionHost"); + GetHealthStatus().IsHealthy.ShouldBeTrue(); + } + + [Test] + public async Task ServiceIsDeployedWithNoNodes_ThrowsEnvironmentException() + { + SetServiceVersion("1.0.0"); + await Init(); + AssertExceptionIsThrown(); + GetHealthStatus().IsHealthy.ShouldBeFalse(); + } + + [Test] + public async Task Disposed_StopMonitoring() + { + AddServiceNode(); + await Init(); + + await WaitForUpdates(); + var healthRequestsCounterBeforeDisposed = _consulSimulator.HealthRequestsCounter; + _nodeSource.Dispose(); + + _consulSimulator.HealthRequestsCounter.ShouldBe(healthRequestsCounterBeforeDisposed, "service monitoring should have been stopped when the service became undeployed"); + GetHealthStatus().IsHealthy.ShouldBeTrue(); + } + + private async Task Init() + { + await WaitForUpdates(); + _nodeSource = await _consulNodeSourceFactory.CreateNodeSource(_deploymentIdentifier); + } + + + private async Task AssertOneDefaultNode() + { + var nodes = _nodeSource.GetNodes(); + nodes.Length.ShouldBe(1); + nodes[0].Hostname.ShouldBe(Host1); + nodes[0].Port.ShouldBe(Port1); + } + + private void AssertExceptionIsThrown() + { + var getNodesAction = (Action) (() => + { + _nodeSource.GetNodes(); + }); + + getNodesAction.ShouldThrow(); + } + + private HealthCheckResult GetHealthStatus() + { + var healthMonitor = (FakeHealthMonitor)_testingKernel.Get(); + return healthMonitor.Monitors["Consul"](); + } + + private async void AddServiceNode(string hostName=Host1, int port=Port1, string version=Version, string serviceName=null) + { + _consulSimulator.AddServiceNode(serviceName ?? _deploymentIdentifier.GetConsulServiceName(), new ConsulEndPoint {HostName = hostName, Port = port, Version = version}); + } + + private async void RemoveServiceEndPoint(string hostName = Host1, int port = Port1) + { + _consulSimulator.RemoveServiceNode(_deploymentIdentifier.GetConsulServiceName(), new ConsulEndPoint { HostName = hostName, Port = port}); + } + + private void SetServiceVersion(string version) + { + _consulSimulator.SetServiceVersion(_deploymentIdentifier.GetConsulServiceName(), version); + } + + private void RemoveService(string serviceName=null) + { + _consulSimulator.RemoveService(serviceName ?? _deploymentIdentifier.GetConsulServiceName()); + } + + private void SetConsulIsDown() + { + _consulSimulator.SetError(new Exception("fake error")); + } + } +} diff --git a/tests/Gigya.Microdot.UnitTests/Discovery/Rewrite/DiscoveryTests.cs b/tests/Gigya.Microdot.UnitTests/Discovery/Rewrite/DiscoveryTests.cs new file mode 100644 index 00000000..1537d792 --- /dev/null +++ b/tests/Gigya.Microdot.UnitTests/Discovery/Rewrite/DiscoveryTests.cs @@ -0,0 +1,295 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Gigya.Microdot.Fakes; +using Gigya.Microdot.Interfaces.SystemWrappers; +using Gigya.Microdot.ServiceDiscovery; +using Gigya.Microdot.ServiceDiscovery.Config; +using Gigya.Microdot.ServiceDiscovery.Rewrite; +using Gigya.Microdot.SharedLogic.Rewrite; +using Gigya.Microdot.Testing.Shared; +using Ninject; +using NSubstitute; +using NUnit.Framework; +using Shouldly; + +namespace Gigya.Microdot.UnitTests.Discovery.Rewrite +{ + [TestFixture] + public class DiscoveryTests + { + private const string Consul = "Consul"; + private const string SlowSource = "SlowSource"; + private const string Config = "Config"; + private const string Local = "Local"; + + private const string ServiceName = "ServiceName"; + private const string Env = "env"; + + private IDiscovery _discovery; + + private TestingKernel _kernel; + private bool _consulSourceWasUndeployed; + private DiscoveryConfig _discoveryConfig; + private List _createdNodeSources; + private DeploymentIdentifier _deploymentIdentifier; + + private Node _consulNode; + private INodeSourceFactory _consulNodeSourceFactory; + + private INodeSource _slowNodeSource; + private INodeSourceFactory _slowNodeSourceFactory; + private TaskCompletionSource _waitForSlowSourceCreation; + private DateTimeFake _dateTimeFake; + private int _consulSourceDisposedCounter; + + + [OneTimeSetUp] + public void SetupKernel() + { + _kernel = new TestingKernel(k => + { + RebindKernelToSetCreatedNodeSourceBeforeCreatingIt(k); + RebindKernelToSetCreatedNodeSourceBeforeCreatingIt(k); + + k.Rebind().ToMethod(c => _consulNodeSourceFactory); + k.Bind().ToMethod(c => _slowNodeSourceFactory); + k.Rebind>().ToMethod(c => () => _discoveryConfig); + k.Rebind().To().InTransientScope(); // get a different instance for each test + k.Rebind().ToMethod(_=>_dateTimeFake); + }); + } + + [OneTimeTearDown] + public void DisposeKernel() + { + _kernel.Dispose(); + } + + private void RebindKernelToSetCreatedNodeSourceBeforeCreatingIt(IKernel kernel) where TNodeSource: INodeSource + { + var createLocalNodeSource = kernel.Get>(); + kernel.Rebind>().ToMethod(_ => di=> + { + _createdNodeSources.Add(typeof(TNodeSource)); + return createLocalNodeSource(di); + }); + } + + [SetUp] + public void Setup() + { + _dateTimeFake = new DateTimeFake(); + + _createdNodeSources = new List(); + SetupConsulNodeSource(); + SetupSlowNodeSource(); + + _discoveryConfig = new DiscoveryConfig(); + _discoveryConfig.Services = new ServiceDiscoveryCollection(new Dictionary(), new ServiceDiscoveryConfig(), new PortAllocationConfig()); + + _discovery = _kernel.Get(); + _deploymentIdentifier = new DeploymentIdentifier(ServiceName, Env, Substitute.For()); + } + + private void SetupConsulNodeSource() + { + _consulSourceDisposedCounter = 0; + + _consulNode = new Node("ConsulNode", 123); + _consulSourceWasUndeployed = false; + + _consulNodeSourceFactory = Substitute.For(); + _consulNodeSourceFactory.Type.Returns(Consul); + _consulNodeSourceFactory.IsServiceDeployed(Arg.Any()).Returns(c=> !_consulSourceWasUndeployed); + _consulNodeSourceFactory.CreateNodeSource(Arg.Any()) + .Returns(_ => + { + _createdNodeSources.Add(typeof(INodeSource)); + return _consulSourceWasUndeployed ? null : CreateNewConsulSource(); + }); + } + + private INodeSource CreateNewConsulSource() + { + var consulSource = Substitute.For(); + consulSource.GetNodes().Returns(new[] {_consulNode}); + consulSource.When(n => ((IDisposable) n).Dispose()).Do(_ => _consulSourceDisposedCounter++); + return consulSource; + } + + private void SetupSlowNodeSource() + { + _waitForSlowSourceCreation = new TaskCompletionSource(); + + _slowNodeSource = Substitute.For(); + _slowNodeSource.GetNodes().Returns(new Node[0] ); + + _slowNodeSourceFactory = Substitute.For(); + _slowNodeSourceFactory.Type.Returns(SlowSource); + _slowNodeSourceFactory.IsServiceDeployed(Arg.Any()).Returns(true); + _slowNodeSourceFactory.CreateNodeSource(Arg.Any()) + .Returns(async _ => + { + _createdNodeSources.Add(typeof(INodeSource)); + await Task.WhenAny(_waitForSlowSourceCreation.Task, Task.Delay(5000)); + return _slowNodeSource; + }); + } + + [Test] + public async Task CreateLoadBalancer_GetNodesFromConsulNodeSource() + { + ConfigureServiceSource(Consul); + var loadBalancer = CreateLoadBalancer(); + (await loadBalancer.GetNode()).ShouldBe(_consulNode); + } + + [Test] + public async Task CreateLoadBalancer_GetNodesFromConfigNodeSource() + { + ConfigureServiceSource(Config); + await CreateLoadBalancer().GetNode(); + _createdNodeSources.Single().ShouldBe(typeof(ConfigNodeSource)); + } + + [Test] + public async Task CreateLoadBalancer_GetNodesFromLocalNodeSource() + { + ConfigureServiceSource(Local); + await CreateLoadBalancer().GetNode(); + _createdNodeSources.Single().ShouldBe(typeof(LocalNodeSource)); + } + + [Test] + public async Task CreateLoadBalancer_ReturnNullIfServiceUndeployed() + { + ConfigureServiceSource(Consul); + _consulSourceWasUndeployed = true; + var loadBalancer = CreateLoadBalancer(); + (await loadBalancer.GetNode()).ShouldBeNull(); + } + + [Test] + public async Task GetNodes_SourceWasUndeployed_ReturnNull() + { + ConfigureServiceSource(Consul); + _consulSourceWasUndeployed = true; + (await GetNodes()).ShouldBeNull(); + } + + [Test] + public async Task GetNodes_SourceWasRedeployed_ReturnNodes() + { + ConfigureServiceSource(Consul); + _consulSourceWasUndeployed = true; + await GetNodes(); + _consulSourceWasUndeployed = false; + (await GetNodes()).ShouldContain(_consulNode); + } + + [Test] + public async Task GetNodes_ConfigurationChanged_ReturnNodesFromNewNodeSource() + { + ConfigureServiceSource(Local); + (await GetNodes()).ShouldNotContain(_consulNode); + + ConfigureServiceSource(Consul); + await WaitForCleanup(); + (await GetNodes()).ShouldContain(_consulNode); + } + + [Test] + public async Task GetNodes_CalledMoreThanOnce_OnlyOneNodeSourceIsCreated() + { + ConfigureServiceSource(SlowSource); + await GetNodesThreeTimesFromSlowSource(); + _createdNodeSources.Count.ShouldBe(1); + } + + [Test] + public async Task GetNodes_CalledMoreThanOnceAfterConfigurationChanged_OnlyOneAdditionalNodeSourceIsCreated() + { + ConfigureServiceSource(Local); + await GetNodes(); + _createdNodeSources.Count.ShouldBe(1); + + ConfigureServiceSource(SlowSource); + await WaitForCleanup(); + await GetNodesThreeTimesFromSlowSource(); + _createdNodeSources.Count.ShouldBe(2); + } + + [Test] + public async Task GetNodes_ServiceUndeployed_DontTryToCreateNewNodeSourceUntilServiceIsRedeployed() + { + ConfigureServiceSource(Consul); + await GetNodes(); + _createdNodeSources.Count.ShouldBe(1); + + _consulSourceWasUndeployed = true; + await WaitForCleanup(); + (await GetNodes()).ShouldBeNull(); + _createdNodeSources.Count.ShouldBe(1); + + _consulSourceWasUndeployed = false; + await WaitForCleanup(); + (await GetNodes()).ShouldNotBeNull(); + _createdNodeSources.Count.ShouldBe(2); + } + + [Test] + public async Task DisposeNodeSourceAfterLifetimeIsPassed() + { + ConfigureServiceSource(Consul); + _discoveryConfig.MonitoringLifetime = TimeSpan.FromMinutes(2); + await GetNodes(); + _createdNodeSources.Count.ShouldBe(1); + + _dateTimeFake.UtcNow += TimeSpan.FromMinutes(3); + await WaitForCleanup(); + await GetNodes(); + // first NodeSource was disposed after being not-in-use for more than 2 minutes. A new NodeSource should have been created + _createdNodeSources.Count.ShouldBe(2); + _consulSourceDisposedCounter.ShouldBe(1); + } + + private async Task WaitForCleanup() + { + await Task.Delay(100); + _dateTimeFake.StopDelay(); + await Task.Delay(100); + } + + private void ConfigureServiceSource(string sourceType) + { + _discoveryConfig.Services[_deploymentIdentifier.ServiceName].Source = sourceType; + if (sourceType == Config) + _discoveryConfig.Services[_deploymentIdentifier.ServiceName].Hosts = "myhost"; + } + + private async Task GetNodesThreeTimesFromSlowSource() + { + var getNodesTasks = Task.WhenAll(GetNodes(), GetNodes(), GetNodes()); + await Task.Delay(100); + SlowSourceCanFinallyBeCreated(); + await getNodesTasks; + } + + private void SlowSourceCanFinallyBeCreated() + { + _waitForSlowSourceCreation.SetResult(true); + } + + private ILoadBalancer CreateLoadBalancer() + { + return _discovery.CreateLoadBalancer(_deploymentIdentifier, null, TrafficRoutingStrategy.RandomByRequestID); + } + + private async Task GetNodes() + { + return await _discovery.GetNodes(_deploymentIdentifier); + } + } +} diff --git a/tests/Gigya.Microdot.UnitTests/Discovery/Rewrite/LoadBalancerTests.cs b/tests/Gigya.Microdot.UnitTests/Discovery/Rewrite/LoadBalancerTests.cs new file mode 100644 index 00000000..423102a3 --- /dev/null +++ b/tests/Gigya.Microdot.UnitTests/Discovery/Rewrite/LoadBalancerTests.cs @@ -0,0 +1,381 @@ +#region Copyright +// Copyright 2017 Gigya Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +#endregion + +using System; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using Gigya.Common.Contracts.Exceptions; +using Gigya.Microdot.Fakes; +using Gigya.Microdot.Interfaces.Logging; +using Gigya.Microdot.Interfaces.SystemWrappers; +using Gigya.Microdot.ServiceDiscovery; +using Gigya.Microdot.ServiceDiscovery.HostManagement; +using Gigya.Microdot.ServiceDiscovery.Rewrite; +using Gigya.Microdot.SharedLogic.Monitor; +using Gigya.Microdot.SharedLogic.Rewrite; +using Gigya.Microdot.Testing.Shared; +using Metrics; + +using Ninject; +using NSubstitute; +using NUnit.Framework; + +using Shouldly; + +namespace Gigya.Microdot.UnitTests.Discovery.Rewrite +{ + [TestFixture] + public class LoadBalancerTests + { + private const int Repeat = 3; + private const string ServiceName = "ServiceName"; + private const string Env = "prod"; + private ILoadBalancer _loadBalancer; + + private LogSpy _log; + + private ReachabilityCheck _reachabilityCheck; + private TestingKernel _kernel; + + private IDiscovery _discovery; + + private Node Node1 = new Node("Host1", 111); + private Node Node2 = new Node("Host2", 222); + private Node Node3 = new Node("Host3", 333); + private Node Node4 = new Node("Host4", 444); + private Node Node5 = new Node("Host5", 555); + private Node Node6 = new Node("Host6", 666); + + private Func _getSourceNodes = () => new Node[0]; + private IEnvironment _environment; + + [OneTimeSetUp] + public void OneTimeSetup() + { + _kernel = new TestingKernel(k=>k.Rebind().ToMethod(_=>_discovery)); + } + + [OneTimeTearDown] + public void OneTimeTearDown() + { + _kernel.Dispose(); + } + + [SetUp] + public void Setup() + { + _log = (LogSpy)_kernel.Get(); + _discovery = Substitute.For(); + _discovery.GetNodes(Arg.Any()).Returns(_ => Task.FromResult(_getSourceNodes())); + _reachabilityCheck = (n,c) => throw new EnvironmentException("node is unreachable"); + _environment = Substitute.For(); + } + + private void CreateLoadBalancer(TrafficRoutingStrategy trafficRoutingStrategy=TrafficRoutingStrategy.RandomByRequestID) + { + var createLoadBalancer = _kernel.Get>(); + _loadBalancer = createLoadBalancer( + new DeploymentIdentifier(ServiceName, Env, _environment), + (n, c) => _reachabilityCheck(n, c), + trafficRoutingStrategy); + } + + [TearDown] + public void TearDown() + { + _loadBalancer?.Dispose(); + } + + [Test] + [Repeat(Repeat)] + public async Task GetNode_RoutingTrafficRoundRobin_GetDiffenent3NodesAfterExactly3Times() + { + CreateLoadBalancer(TrafficRoutingStrategy.RoundRobin); + SetupDefaultNodes(); + var allEndpoints = await GetNodes(times:3); + allEndpoints.ShouldContain(Node1); + allEndpoints.ShouldContain(Node2); + allEndpoints.ShouldContain(Node3); + } + + [Test] + public async Task GetNode_ThreeNodes_ReturnsAllThree() + { + CreateLoadBalancer(); + SetupDefaultNodes(); + + var allEndpoints = await Get20Nodes(); + + new[] { Node1, Node2, Node3 }.ShouldBeSubsetOf(allEndpoints); + } + + [Test] + public void GetNode_ThreeNodes_ShouldBeHealthy() + { + CreateLoadBalancer(); + SetupDefaultNodes(); + + _loadBalancer.GetNode(); + var healthStatus = GetHealthStatus(); + + healthStatus.IsHealthy.ShouldBeTrue(); + } + + [Test] + public async Task GetNode_NodesChanged_ReturnsNewNodes() + { + CreateLoadBalancer(); + SetupSourceNodes(Node1,Node2,Node3); + Get20Nodes(); + SetupSourceNodes(Node4, Node5, Node6); + + + var res = await Get20Nodes(); + res.Distinct() + .ShouldBe(new[] { Node4, Node5, Node6 }, true); + } + + [Test] + public void GetNode_NoNodes_Throws() + { + CreateLoadBalancer(); + SetupNoNodes(); + Should.Throw(() =>_loadBalancer.GetNode()); + } + + [Test] + [Repeat(Repeat)] + public void GetNode_NodesListBecomesEmpty_Throws() + { + CreateLoadBalancer(); + SetupDefaultNodes(); + Get20Nodes(); + SetupNoNodes(); + Should.Throw(() => _loadBalancer.GetNode()); + var healthStatus = GetHealthStatus(); + healthStatus.IsHealthy.ShouldBeFalse(); + } + + [Test] + [Repeat(Repeat)] + public async Task GetNode_AfterNodeReportedUnreachable_NodeWillNotBeReturned() + { + CreateLoadBalancer(); + var allNodes = new[] {Node1, Node2, Node3}; + SetupSourceNodes(allNodes); + + var unreachableNode = await _loadBalancer.GetNode(); + _loadBalancer.ReportUnreachable(unreachableNode); + + var nodes = await Get20Nodes(); + foreach (var node in allNodes) + { + if (node.Equals(unreachableNode)) + nodes.ShouldNotContain(node); + else + nodes.ShouldContain(node); + } + } + + [Test] + [Repeat(Repeat)] + public async Task GetNode_NodeIsReachableAgain_NodeWillBeReturned() + { + CreateLoadBalancer(); + SetupDefaultNodes(); + + var selectedNode = await _loadBalancer.GetNode(); + _loadBalancer.ReportUnreachable(selectedNode); + + (await Get20Nodes()).ShouldNotContain(selectedNode); + + _reachabilityCheck = (_,__) => Task.FromResult(true); + await Task.Delay(1000); + + (await Get20Nodes()).ShouldContain(selectedNode); + } + + [Test] + [Repeat(Repeat)] + public async Task GetNode_OnlyOneNodeUnreachable_ShouldStillBeHealthy() + { + CreateLoadBalancer(); + SetupDefaultNodes(); + + await Run20times(node => + { + if (node.Equals(Node2)) + _loadBalancer.ReportUnreachable(node); + }); + + var healthResult = GetHealthStatus(); + healthResult.IsHealthy.ShouldBeTrue(); + healthResult.Message.ShouldContain(Node2.ToString()); + } + + + private async Task Run20times(Action act) + { + for (int i = 0; i < 20; i++) + { + try + { + var node = await _loadBalancer.GetNode(); + act(node); + } + catch + { + } + } + } + + [Test] + [Repeat(Repeat)] + public async Task GetNode_NodeUnreachableThenReturnsInBackground_NodeShouldBeReturned() + { + CreateLoadBalancer(); + SetupDefaultNodes(); + _reachabilityCheck = (_,__) => throw new EnvironmentException("node is unreachable"); + + var selectedNode = await _loadBalancer.GetNode(); + _loadBalancer.ReportUnreachable(selectedNode); + + (await Get20Nodes()).ShouldNotContain(selectedNode); + + var waitForReachablitiy = new TaskCompletionSource(); + _reachabilityCheck = (_,__) => + { + waitForReachablitiy.SetResult(true); + return Task.FromResult(true); + }; + await waitForReachablitiy.Task; + await Task.Delay(50); + + (await Get20Nodes()).ShouldContain(selectedNode); + } + + [Test] + [Repeat(Repeat)] + public async Task GetNode_AllNodesUnreachable_ThrowsException() + { + CreateLoadBalancer(); + SetupSourceNodes(Node1,Node2,Node3); + + await Run20times(node =>_loadBalancer.ReportUnreachable(node)); + + Should.Throw(() => _loadBalancer.GetNode()); + var healthStatus = GetHealthStatus(); + healthStatus.IsHealthy.ShouldBeFalse(healthStatus.Message); + healthStatus.Message.ShouldContain("All 3 Nodes are unreachable"); + healthStatus.Message.ShouldContain(Node1.ToString()); + healthStatus.Message.ShouldContain(Node2.ToString()); + healthStatus.Message.ShouldContain(Node3.ToString()); + } + + [Test] + [Repeat(Repeat)] + public async Task GetNode_AllNodesUnreachableThenAllNodesReachable_ReturnsAllNodes() + { + CreateLoadBalancer(); + SetupSourceNodes(Node1,Node2,Node3); + await Run20times(node => _loadBalancer.ReportUnreachable(node)); + + Should.Throw(() => _loadBalancer.GetNode()); + + _reachabilityCheck = (_,__) => Task.FromResult(true); + + await Task.Delay(1000); + + var nodes = await Get20Nodes(); + nodes.ShouldContain(Node1); + nodes.ShouldContain(Node2); + nodes.ShouldContain(Node3); + } + + + [Test] + [Repeat(Repeat)] + public async Task GetNode_NodesUnreachableButReachabilityCheckThrows_ErrorIsLogged() + { + CreateLoadBalancer(); + SetupDefaultNodes(); + var reachabilityException = new Exception("Simulated error while running reachability check"); + + _reachabilityCheck = (_,__) => { throw reachabilityException; }; + await Run20times(node => _loadBalancer.ReportUnreachable(node)); + + await Task.Delay(1500); + + _log.LogEntries.ToArray().ShouldContain(e => e.Exception == reachabilityException); + } + + [Test] + public async Task ErrorGettingNodes_MatchingExceptionIsThrown() + { + CreateLoadBalancer(); + var expectedException = new EnvironmentException("Error getting nodes"); + SetupErrorGettingNodes(expectedException); + var actualException = Should.Throw(() => _loadBalancer.GetNode(), "No nodes were discovered for service"); + actualException.InnerException.ShouldBe(expectedException); + } + + private void SetupNoNodes() + { + SetupSourceNodes( /* no nodes */); + } + + private void SetupSourceNodes(params Node[] nodes) + { + _getSourceNodes = () => nodes; + } + + private void SetupErrorGettingNodes(Exception ex) + { + _getSourceNodes = () => throw ex; + } + + private void SetupDefaultNodes() + { + SetupSourceNodes(Node1, Node2, Node3); + } + + async Task GetNodes(int times) + { + var tasks = Enumerable.Repeat(1, times).Select(_ => _loadBalancer.GetNode()); + await Task.WhenAll(tasks); + return tasks.Select(t => t.Result).ToArray(); + } + + Task Get20Nodes() + { + return GetNodes(20); + } + + private HealthCheckResult GetHealthStatus() + { + var healthMonitor = (FakeHealthMonitor)_kernel.Get(); + return healthMonitor.Monitors[new DeploymentIdentifier(ServiceName,Env, _environment).ToString()].Invoke(); + } + } + +} diff --git a/tests/Gigya.Microdot.UnitTests/Discovery/Rewrite/NewConsulDiscoveryMasterFallBackTest.cs b/tests/Gigya.Microdot.UnitTests/Discovery/Rewrite/NewConsulDiscoveryMasterFallBackTest.cs new file mode 100644 index 00000000..d779bd15 --- /dev/null +++ b/tests/Gigya.Microdot.UnitTests/Discovery/Rewrite/NewConsulDiscoveryMasterFallBackTest.cs @@ -0,0 +1,251 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading.Tasks; +using Gigya.Common.Contracts.Exceptions; +using Gigya.Microdot.Fakes; +using Gigya.Microdot.Interfaces.Configuration; +using Gigya.Microdot.Interfaces.SystemWrappers; +using Gigya.Microdot.ServiceDiscovery; +using Gigya.Microdot.ServiceDiscovery.Rewrite; +using Gigya.Microdot.SharedLogic.Rewrite; +using Gigya.Microdot.Testing.Shared; +using Ninject; +using NSubstitute; +using NUnit.Framework; +using Shouldly; + +namespace Gigya.Microdot.UnitTests.Discovery.Rewrite +{ + [TestFixture] + public class NewConsulDiscoveryMasterFallBackTest + { + private const string ServiceVersion = "1.2.30.1234"; + private string _serviceName; + private const string MASTER_ENVIRONMENT = "prod"; + private const string ORIGINATING_ENVIRONMENT = "fake_env"; + private Dictionary _configDic; + private TestingKernel _unitTestingKernel; + private Dictionary _loadBalancers; + private Dictionary> _nodeResults; + private HashSet _consulServiceList; + private IEnvironment _environment; + private IDateTime _dateTimeMock; + private int id; + private const int Repeat = 1; + + [SetUp] + public void SetUp() + { + _unitTestingKernel?.Dispose(); + _serviceName = $"ServiceName{++id}"; + + _environment = Substitute.For(); + _environment.Zone.Returns("il3"); + _environment.DeploymentEnvironment.Returns(ORIGINATING_ENVIRONMENT); + _environment.ConsulAddress.Returns((string)null); + + _configDic = new Dictionary { { "Discovery.EnvironmentFallbackEnabled", "true" } }; + _unitTestingKernel = new TestingKernel(k => + { + k.Rebind().ToConstant(_environment); + k.Rebind().To().InSingletonScope(); + + SetupConsulMocks(k); + + _dateTimeMock = Substitute.For(); + _dateTimeMock.Delay(Arg.Any()).Returns(c => Task.Delay(TimeSpan.FromMilliseconds(100))); + k.Rebind().ToConstant(_dateTimeMock); + }, _configDic); + + var environment = _unitTestingKernel.Get(); + Assert.AreEqual(_environment, environment); + } + + private void SetupConsulMocks(IKernel kernel) + { + _loadBalancers = new Dictionary(); + _nodeResults = new Dictionary>(); + + _consulServiceList = new HashSet(); + + var discovery = Substitute.For(); + discovery.CreateLoadBalancer(Arg.Any(), Arg.Any(), TrafficRoutingStrategy.RandomByRequestID).Returns(c => GetLoadBalancerMock(c.Arg())); + kernel.Rebind().ToMethod(_ => discovery); + + CreateConsulMock(MasterService); + CreateConsulMock(OriginatingService); + + } + + private ILoadBalancer CreateLoadBalancerMock(DeploymentIdentifier di) + { + var mock = Substitute.For(); + mock.GetNode().Returns(_ => _consulServiceList.Contains(di) ? _nodeResults[di]() : null); + return mock; + } + + private ILoadBalancer GetLoadBalancerMock(DeploymentIdentifier di) + { + if (_loadBalancers.ContainsKey(di)) + return _loadBalancers[di]; + return CreateLoadBalancerMock(di); + } + + private void CreateConsulMock(DeploymentIdentifier di) + { + var mock = CreateLoadBalancerMock(di); + + _nodeResults[di] = () => new ConsulNode(hostName: "dummy", version: ServiceVersion) ; + _loadBalancers[di] = mock; + + _consulServiceList.Add(di); + } + + [TearDown] + public void TearDown() + { + _unitTestingKernel.Dispose(); + } + + [Test] + [Repeat(Repeat)] + public async Task ServiceNotExistsShouldFallBackToMaster() + { + SetMockToReturnHost(MasterService); + SetMockToReturnServiceNotDefined(OriginatingService); + var nextHost = await GetServiceDiscovery().GetNode(); + nextHost.Hostname.ShouldBe(HostnameFor(MasterService)); + } + + [Test] + [Repeat(Repeat)] + public async Task FallbackDisabledByConsul_ShouldNotFallBackToMaster() + { + _configDic[$"Discovery.EnvironmentFallbackEnabled"] = "false"; + + SetMockToReturnHost(MasterService); + SetMockToReturnServiceNotDefined(OriginatingService); + + Should.ThrowAsync(()=>GetServiceDiscovery().GetNode()); + } + + [Test] + [Repeat(Repeat)] + public async Task WhenServiceDeletedShouldFallBackToMaster() + { + var reloadInterval = TimeSpan.FromMilliseconds(5); + _configDic[$"Discovery.Services.{_serviceName}.ReloadInterval"] = reloadInterval.ToString(); + + SetMockToReturnHost(MasterService); + SetMockToReturnHost(OriginatingService); + + var discovey = GetServiceDiscovery(); + + var node = await discovey.GetNode(); + node.Hostname.ShouldBe(HostnameFor(OriginatingService)); + + SetMockToReturnServiceNotDefined(OriginatingService); + + + node = await discovey.GetNode(); + node.Hostname.ShouldBe(HostnameFor(MasterService)); + } + + [Test] + [Repeat(Repeat)] + public async Task WhenServiceAddedShouldNotFallBackToMaster() + { + var reloadInterval = TimeSpan.FromMilliseconds(5); + _configDic[$"Discovery.Services.{_serviceName}.ReloadInterval"] = reloadInterval.ToString(); + + SetMockToReturnHost(MasterService); + SetMockToReturnServiceNotDefined(OriginatingService); + + var discovey = GetServiceDiscovery(); + + var node = await discovey.GetNode(); + node.Hostname.ShouldBe(HostnameFor(MasterService)); + + SetMockToReturnHost(OriginatingService); + + node = await discovey.GetNode(); + node.Hostname.ShouldBe(HostnameFor(OriginatingService)); + } + + [Test] + [Repeat(Repeat)] + public async Task ShouldNotFallBackToMasterOnConsulError() + { + SetMockToReturnHost(MasterService); + SetMockToReturnError(OriginatingService); + Should.Throw(async () => await GetServiceDiscovery().GetNode()); + } + + [Test] + [Repeat(Repeat)] + public async Task ServiceExistsShouldNotFallBackToMaster() + { + SetMockToReturnHost(MasterService); + SetMockToReturnHost(OriginatingService); + + var nextHost = await GetServiceDiscovery().GetNode(); + nextHost.Hostname.ShouldBe(HostnameFor(OriginatingService)); + } + + [Test] + [Repeat(Repeat)] + public void MasterShouldNotFallBack() + { + _environment = Substitute.For(); + _environment.Zone.Returns("il3"); + _environment.DeploymentEnvironment.Returns(MASTER_ENVIRONMENT); + _unitTestingKernel.Rebind().ToConstant(_environment); + + SetMockToReturnServiceNotDefined(MasterService); + + Should.ThrowAsync(() => GetServiceDiscovery().GetNode()); + } + + private void SetMockToReturnHost(DeploymentIdentifier di) + { + if (!_loadBalancers.ContainsKey(di)) + CreateConsulMock(di); + + var newNode = new Node(HostnameFor(di)); + _nodeResults[di] = () => newNode; + + _consulServiceList.Add(di); + } + + private void SetMockToReturnServiceNotDefined(DeploymentIdentifier di) + { + _consulServiceList.Remove(di); + } + + private void SetMockToReturnError(DeploymentIdentifier di) + { + _nodeResults[di] = () => throw new EnvironmentException("Mock: some error"); + } + + [Test] + public void ServiceDiscoveySameNameShouldBeTheSame() + { + Assert.AreEqual(GetServiceDiscovery(), GetServiceDiscovery()); + } + + private INewServiceDiscovery GetServiceDiscovery() + { + var discovery = + _unitTestingKernel.Get>()(_serviceName, + (n, c) => Task.FromResult(true)); + return discovery; + } + + + private DeploymentIdentifier MasterService => new DeploymentIdentifier(_serviceName, MASTER_ENVIRONMENT, _environment); + private DeploymentIdentifier OriginatingService => new DeploymentIdentifier(_serviceName, ORIGINATING_ENVIRONMENT, _environment); + private string HostnameFor(DeploymentIdentifier di) => $"{di.DeploymentEnvironment}-host"; + + } +} \ No newline at end of file diff --git a/tests/Gigya.Microdot.UnitTests/Discovery/Rewrite/NewServiceDiscoveryConfigChangeTest.cs b/tests/Gigya.Microdot.UnitTests/Discovery/Rewrite/NewServiceDiscoveryConfigChangeTest.cs new file mode 100644 index 00000000..32cfa513 --- /dev/null +++ b/tests/Gigya.Microdot.UnitTests/Discovery/Rewrite/NewServiceDiscoveryConfigChangeTest.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Threading.Tasks.Dataflow; +using Gigya.Microdot.Configuration; +using Gigya.Microdot.Fakes; +using Gigya.Microdot.Interfaces.Configuration; +using Gigya.Microdot.Interfaces.SystemWrappers; +using Gigya.Microdot.ServiceDiscovery; +using Gigya.Microdot.ServiceDiscovery.Config; +using Gigya.Microdot.ServiceDiscovery.Rewrite; +using Gigya.Microdot.SharedLogic; +using Gigya.Microdot.SharedLogic.SystemWrappers; +using Gigya.Microdot.Testing.Shared; +using Ninject; +using NUnit.Framework; +using Shouldly; +using IConsulClient = Gigya.Microdot.ServiceDiscovery.IConsulClient; + +namespace Gigya.Microdot.UnitTests.Discovery.Rewrite +{ + [TestFixture] + public class NewServiceDiscoveryConfigChangeTest + { + private const string ServiceName = "ServiceName"; + private NewServiceDiscovery _serviceDiscovery; + private Dictionary _configDic; + private TestingKernel _unitTestingKernel; + private ConsulClientMock _consulClientMock; + private DiscoveryConfig _discoveryConfig; + public const int Repeat = 1; + private const string ServiceVersion = "1.0.0.0"; + + [SetUp] + public async Task Setup() + { + _configDic = new Dictionary(); + _unitTestingKernel = new TestingKernel(k => + { + k.Rebind().To(); + k.Rebind().To(); + k.Rebind>().ToMethod(_ => () => _discoveryConfig); + _consulClientMock = new ConsulClientMock(); + _consulClientMock.SetResult(new EndPointsResult { EndPoints = new[] { new ConsulEndPoint { HostName = "dumy", Version = ServiceVersion } }, ActiveVersion = ServiceVersion, IsQueryDefined = true }); + k.Rebind>().ToMethod(c=>s=>_consulClientMock); + }, _configDic); + + _discoveryConfig = new DiscoveryConfig { Services = new ServiceDiscoveryCollection(new Dictionary(), new ServiceDiscoveryConfig(), new PortAllocationConfig()) }; + _serviceDiscovery = _unitTestingKernel.Get>()("ServiceName", x => Task.FromResult(true)); + } + + [TearDown] + public void TearDown() + { + _unitTestingKernel.Dispose(); + _consulClientMock.Dispose(); + } + + [Repeat(Repeat)] + + public async Task DiscoveySettingAreUpdateOnConfigChange() + { + _discoveryConfig.Services[ServiceName].Source = "Config"; + _discoveryConfig.Services[ServiceName].Hosts = "host3"; + + var node = await _serviceDiscovery.GetNode(); + Assert.AreEqual("Config", _serviceDiscovery.LastServiceConfig.Source); + Assert.AreEqual("host3", node.Hostname); + } + + [Repeat(Repeat)] + + public async Task ServiceSourceIsLocal() + { + _discoveryConfig.Services[ServiceName].Source = "Local"; + var node = await _serviceDiscovery.GetNode(); + node.Hostname.ShouldContain(CurrentApplicationInfo.HostName); + } + + } +} \ No newline at end of file diff --git a/tests/Gigya.Microdot.UnitTests/Discovery/ServiceDiscoveryConfigChangeTest.cs b/tests/Gigya.Microdot.UnitTests/Discovery/ServiceDiscoveryConfigChangeTest.cs index d40e69dc..a508fae8 100644 --- a/tests/Gigya.Microdot.UnitTests/Discovery/ServiceDiscoveryConfigChangeTest.cs +++ b/tests/Gigya.Microdot.UnitTests/Discovery/ServiceDiscoveryConfigChangeTest.cs @@ -5,9 +5,11 @@ using Gigya.Microdot.Configuration; using Gigya.Microdot.Fakes; using Gigya.Microdot.Interfaces.Configuration; +using Gigya.Microdot.Interfaces.SystemWrappers; using Gigya.Microdot.ServiceDiscovery; using Gigya.Microdot.ServiceDiscovery.Config; using Gigya.Microdot.SharedLogic; +using Gigya.Microdot.SharedLogic.SystemWrappers; using Gigya.Microdot.Testing; using Gigya.Microdot.Testing.Shared; using Gigya.Microdot.Testing.Shared.Utils; @@ -39,7 +41,7 @@ public async Task Setup() _unitTestingKernel = new TestingKernel(k => { k.Rebind().To().InSingletonScope(); - k.Rebind().To(); + k.Rebind().To(); _consulClientMock = new ConsulClientMock(); _consulClientMock.SetResult(new EndPointsResult { EndPoints = new[] { new ConsulEndPoint { HostName = "dumy", Version = ServiceVersion } }, ActiveVersion = ServiceVersion, IsQueryDefined = true }); k.Rebind>().ToMethod(c=>s=>_consulClientMock); @@ -123,7 +125,7 @@ private async Task WaitForConfigChange(Action update) update(); _configRefresh.RaiseChangeEvent(); - await task; + await task; } } } \ No newline at end of file diff --git a/tests/Gigya.Microdot.UnitTests/Events/EventSerializationTests.cs b/tests/Gigya.Microdot.UnitTests/Events/EventSerializationTests.cs index aba52a2c..e46efe44 100644 --- a/tests/Gigya.Microdot.UnitTests/Events/EventSerializationTests.cs +++ b/tests/Gigya.Microdot.UnitTests/Events/EventSerializationTests.cs @@ -7,6 +7,7 @@ using Gigya.Microdot.Hosting.Events; using Gigya.Microdot.Interfaces.Configuration; using Gigya.Microdot.Interfaces.Events; +using Gigya.Microdot.Interfaces.SystemWrappers; using Gigya.Microdot.SharedLogic; using Gigya.Microdot.SharedLogic.Events; using Gigya.Microdot.SharedLogic.Exceptions; @@ -18,10 +19,10 @@ namespace Gigya.Microdot.UnitTests.Events public class EventSerializationTests { - EventSerializer SerializerWithStackTrace { get; } = new EventSerializer(() => new EventConfiguration(), new NullEnvironmentsVariableProvider(), - new StackTraceEnhancer(() => new StackTraceEnhancerSettings(), new NullEnvironmentsVariableProvider()), () => new EventConfiguration()); - EventSerializer SerializerWithoutStackTrace { get; } = new EventSerializer(() => new EventConfiguration { ExcludeStackTraceRule = new Regex(".*") }, new NullEnvironmentsVariableProvider(), - new StackTraceEnhancer(() => new StackTraceEnhancerSettings(), new NullEnvironmentsVariableProvider()), () => new EventConfiguration()); + EventSerializer SerializerWithStackTrace { get; } = new EventSerializer(() => new EventConfiguration(), new NullEnvironment(), + new StackTraceEnhancer(() => new StackTraceEnhancerSettings(), new NullEnvironment()), () => new EventConfiguration()); + EventSerializer SerializerWithoutStackTrace { get; } = new EventSerializer(() => new EventConfiguration { ExcludeStackTraceRule = new Regex(".*") }, new NullEnvironment(), + new StackTraceEnhancer(() => new StackTraceEnhancerSettings(), new NullEnvironment()), () => new EventConfiguration()); [OneTimeSetUp] public void OneTimeSetUp() @@ -205,11 +206,16 @@ public async Task PublishClientCallEvent() } - internal class NullEnvironmentsVariableProvider : IEnvironmentVariableProvider + internal class NullEnvironment : IEnvironment { - public string DataCenter => nameof(DataCenter); + public string Zone => nameof(Zone); + public string Region => nameof(Region); public string DeploymentEnvironment => nameof(DeploymentEnvironment); public string ConsulAddress => nameof(ConsulAddress); + + [Obsolete("To be deleted on version 2.0")] public string GetEnvironmentVariable(string name) => name; + [Obsolete("To be deleted on version 2.0")] + public void SetEnvironmentVariableForProcess(string name, string value) {} } } \ No newline at end of file diff --git a/tests/Gigya.Microdot.UnitTests/Gigya.Microdot.UnitTests.csproj b/tests/Gigya.Microdot.UnitTests/Gigya.Microdot.UnitTests.csproj index 791efb2e..467a0d1c 100644 --- a/tests/Gigya.Microdot.UnitTests/Gigya.Microdot.UnitTests.csproj +++ b/tests/Gigya.Microdot.UnitTests/Gigya.Microdot.UnitTests.csproj @@ -51,17 +51,25 @@ - + + + + + + + + + + - diff --git a/tests/Gigya.Microdot.UnitTests/HttpServiceRequestTests.cs b/tests/Gigya.Microdot.UnitTests/HttpServiceRequestTests.cs index 6de526aa..07273cff 100644 --- a/tests/Gigya.Microdot.UnitTests/HttpServiceRequestTests.cs +++ b/tests/Gigya.Microdot.UnitTests/HttpServiceRequestTests.cs @@ -2,9 +2,7 @@ using System.Reflection; using FluentAssertions; - -using Gigya.Microdot.Interfaces.HttpService; - +using Gigya.Microdot.SharedLogic.HttpService; using Newtonsoft.Json; using NUnit.Framework; diff --git a/tests/Gigya.Microdot.UnitTests/IDemoService.cs b/tests/Gigya.Microdot.UnitTests/IDemoService.cs index a8a4d766..5cf1154d 100644 --- a/tests/Gigya.Microdot.UnitTests/IDemoService.cs +++ b/tests/Gigya.Microdot.UnitTests/IDemoService.cs @@ -5,7 +5,7 @@ namespace Gigya.Microdot.UnitTests { - [HttpService(5555, Name = AbstractServiceProxyTest.SERVICE_NAME)] + [HttpService(5555)] public interface IDemoService { Task DoSomething(); @@ -15,7 +15,7 @@ public interface IDemoService Task IncrementInt(int val); } - [HttpService(6555, UseHttps = true, Name = AbstractServiceProxyTest.SERVICE_NAME)] + [HttpService(6555, UseHttps = true)] public interface IDemoServiceSecure { Task DoSomething(); diff --git a/tests/Gigya.Microdot.UnitTests/ServiceListenerTests/HttpServiceListenerTests.cs b/tests/Gigya.Microdot.UnitTests/ServiceListenerTests/HttpServiceListenerTests.cs index 95f227f0..7ab2e7dd 100644 --- a/tests/Gigya.Microdot.UnitTests/ServiceListenerTests/HttpServiceListenerTests.cs +++ b/tests/Gigya.Microdot.UnitTests/ServiceListenerTests/HttpServiceListenerTests.cs @@ -41,14 +41,25 @@ public class HttpServiceListenerTests private TestingHost _testinghost; private Task _stopTask; private JsonExceptionSerializer _exceptionSerializer; + private TestingKernel _kernel; + [OneTimeSetUp] + public void OneTimeSetup() + { + _kernel = new TestingKernel(); + } + + [OneTimeTearDown] + public void OneTimeTearDown() + { + _kernel?.Dispose(); + } [SetUp] public virtual void SetUp() { - var kernel = new TestingKernel(); - _insecureClient = kernel.Get(); - _exceptionSerializer = kernel.Get(); + _insecureClient = _kernel.Get(); + _exceptionSerializer = _kernel.Get(); Metric.ShutdownContext("Service"); TracingContext.SetUpStorage(); diff --git a/tests/Gigya.Microdot.UnitTests/ServiceListenerTests/PortsAllocationTests.cs b/tests/Gigya.Microdot.UnitTests/ServiceListenerTests/PortsAllocationTests.cs index fa5643ba..2f1615a9 100644 --- a/tests/Gigya.Microdot.UnitTests/ServiceListenerTests/PortsAllocationTests.cs +++ b/tests/Gigya.Microdot.UnitTests/ServiceListenerTests/PortsAllocationTests.cs @@ -7,11 +7,11 @@ using Gigya.Microdot.Fakes; using Gigya.Microdot.Hosting.HttpService; using Gigya.Microdot.Interfaces; -using Gigya.Microdot.Interfaces.HttpService; using Gigya.Microdot.Ninject; using Gigya.Microdot.ServiceProxy; using Gigya.Microdot.SharedLogic; using Gigya.Microdot.SharedLogic.Exceptions; +using Gigya.Microdot.SharedLogic.HttpService; using Gigya.Microdot.Testing; using Gigya.Microdot.Testing.Shared; using Gigya.Microdot.UnitTests.ServiceProxyTests; @@ -43,7 +43,7 @@ public async Task For_ServiceProxy_TakeDefaultSlot() }); serviceProxy.HttpMessageHandler = handlerMock; - await serviceProxy.Invoke(new HttpServiceRequest("myMethod", new Dictionary()), typeof(int?)); + await serviceProxy.Invoke(new HttpServiceRequest("myMethod", null, new Dictionary()), typeof(int?)); } diff --git a/tests/Gigya.Microdot.UnitTests/ServiceProxyTests/AbstractServiceProxyTest.cs b/tests/Gigya.Microdot.UnitTests/ServiceProxyTests/AbstractServiceProxyTest.cs index 36247e5d..2e385f6e 100644 --- a/tests/Gigya.Microdot.UnitTests/ServiceProxyTests/AbstractServiceProxyTest.cs +++ b/tests/Gigya.Microdot.UnitTests/ServiceProxyTests/AbstractServiceProxyTest.cs @@ -20,8 +20,7 @@ namespace Gigya.Microdot.UnitTests.ServiceProxyTests [TestFixture] public abstract class AbstractServiceProxyTest - { - internal const string SERVICE_NAME = "Demonstration"; + { protected TestingKernel unitTesting; protected Dictionary MockConfig { get; } = new Dictionary(); protected JsonExceptionSerializer ExceptionSerializer { get; set; } diff --git a/tests/Gigya.Microdot.UnitTests/ServiceProxyTests/BehaviorTests.cs b/tests/Gigya.Microdot.UnitTests/ServiceProxyTests/BehaviorTests.cs index 5da648c0..542ef4ef 100644 --- a/tests/Gigya.Microdot.UnitTests/ServiceProxyTests/BehaviorTests.cs +++ b/tests/Gigya.Microdot.UnitTests/ServiceProxyTests/BehaviorTests.cs @@ -9,12 +9,12 @@ using Gigya.Common.Application.HttpService.Client; using Gigya.Common.Contracts.Exceptions; using Gigya.Microdot.Fakes; -using Gigya.Microdot.Interfaces.HttpService; using Gigya.Microdot.ServiceDiscovery; using Gigya.Microdot.ServiceDiscovery.HostManagement; using Gigya.Microdot.ServiceProxy; using Gigya.Microdot.SharedLogic.Events; using Gigya.Microdot.SharedLogic.Exceptions; +using Gigya.Microdot.SharedLogic.HttpService; using Gigya.Microdot.Testing; using Gigya.Microdot.Testing.Shared; using Newtonsoft.Json; @@ -59,7 +59,7 @@ public async Task AllRequestsForSameCallID_SameHostSelected() //If we set Request Id we would like always to select same Host TracingContext.SetRequestID("dumyId1"); - var request = new HttpServiceRequest("testMethod", new Dictionary()); + var request = new HttpServiceRequest("testMethod", null, new Dictionary()); var hostOfFirstReq = (string)await serviceProxy.Invoke(request, typeof(string)); string host; for (int i = 0; i < 50; i++) @@ -109,7 +109,7 @@ public async Task RequestContextShouldOverridePortAndHost() TracingContext.SetHostOverride(serviceName, overrideHost, overridePort); - var request = new HttpServiceRequest("testMethod", new Dictionary()); + var request = new HttpServiceRequest("testMethod", null, new Dictionary()); for (int i = 0; i < 50; i++) { var host = (string)await serviceProxy.Invoke(request, typeof(string)); @@ -148,7 +148,7 @@ public async Task RequestContextShouldOverrideHostOnly() TracingContext.SetHostOverride(serviceName, overrideHost); - var request = new HttpServiceRequest("testMethod", new Dictionary()); + var request = new HttpServiceRequest("testMethod", null, new Dictionary()); for (int i = 0; i < 50; i++) { var host = (string)await serviceProxy.Invoke(request, typeof(string)); @@ -157,7 +157,7 @@ public async Task RequestContextShouldOverrideHostOnly() } - + [Test] public async Task AllHostsAreHavingNetworkErrorsShouldTryEachTwice() { @@ -192,7 +192,7 @@ public async Task AllHostsAreHavingNetworkErrorsShouldTryEachTwice() serviceProxy.HttpMessageHandler = messageHandler; - var request = new HttpServiceRequest("testMethod", new Dictionary()); + var request = new HttpServiceRequest("testMethod", null, new Dictionary()); Func act = () => serviceProxy.Invoke(request, typeof(string)); await act.ShouldThrowAsync(); @@ -227,20 +227,20 @@ public async Task OneHostHasNetworkErrorShouldMoveToNextHost() var messageHandler = new MockHttpMessageHandler(); messageHandler .When("*") - .Respond( req => + .Respond(req => { - bool disableReachabilityChecker = req.Content==null; - if(disableReachabilityChecker) throw new HttpRequestException(); + bool disableReachabilityChecker = req.Content == null; + if (disableReachabilityChecker) throw new HttpRequestException(); counter++; - - if ( req.RequestUri.Host == "host1") throw new HttpRequestException(); + + if (req.RequestUri.Host == "host1") throw new HttpRequestException(); return HttpResponseFactory.GetResponse(content: $"'{req.RequestUri.Host}'"); }); serviceProxy.HttpMessageHandler = messageHandler; - var request = new HttpServiceRequest("testMethod", new Dictionary()); + var request = new HttpServiceRequest("testMethod", null, new Dictionary()); for (int i = 0; i < 3; i++) { @@ -293,7 +293,7 @@ public async Task RequestContextOverrideShouldFailOnFirstAttempt() TracingContext.SetHostOverride("DemoService", overrideHost, overridePort); serviceProxy.HttpMessageHandler = messageHandler; - var request = new HttpServiceRequest("testMethod", new Dictionary()); + var request = new HttpServiceRequest("testMethod", null, new Dictionary()); for (int i = 0; i < 3; i++) { @@ -305,7 +305,7 @@ public async Task RequestContextOverrideShouldFailOnFirstAttempt() } } - + [Test] public async Task FailedHostShouldBeRemovedFromHostList() { @@ -344,7 +344,7 @@ public async Task FailedHostShouldBeRemovedFromHostList() serviceProxy.HttpMessageHandler = messageHandler; - var request = new HttpServiceRequest("testMethod", new Dictionary()); + var request = new HttpServiceRequest("testMethod", null, new Dictionary()); for (int i = 0; i < 10; i++) { @@ -357,7 +357,7 @@ public async Task FailedHostShouldBeRemovedFromHostList() } - + [Test] public async Task ToUpper_MethodCallSucceeds_ResultIsCorrect() diff --git a/tests/Gigya.Microdot.UnitTests/ServiceProxyTests/MetricsTests.cs b/tests/Gigya.Microdot.UnitTests/ServiceProxyTests/MetricsTests.cs index a3b10b15..b4f8893c 100644 --- a/tests/Gigya.Microdot.UnitTests/ServiceProxyTests/MetricsTests.cs +++ b/tests/Gigya.Microdot.UnitTests/ServiceProxyTests/MetricsTests.cs @@ -198,7 +198,7 @@ private static MetricsData GetMetricsData() { return Metric.Context(ServiceProxyProvider.METRICS_CONTEXT_NAME) - .Context(SERVICE_NAME) + .Context("DemoService") .DataProvider.CurrentMetricsData; } From 0ee6bc73a94401cffb8dc089c0ce1e9267211a29 Mon Sep 17 00:00:00 2001 From: David Bronshtein Date: Sun, 7 Oct 2018 14:38:12 +0300 Subject: [PATCH 4/4] increase version number --- SolutionVersion.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SolutionVersion.cs b/SolutionVersion.cs index a3694190..609fbb38 100644 --- a/SolutionVersion.cs +++ b/SolutionVersion.cs @@ -28,9 +28,9 @@ [assembly: AssemblyCopyright("© 2018 Gigya Inc.")] [assembly: AssemblyDescription("Microdot Framework")] -[assembly: AssemblyVersion("1.11.1.0")] -[assembly: AssemblyFileVersion("1.11.1.0")] -[assembly: AssemblyInformationalVersion("1.11.1.0")] +[assembly: AssemblyVersion("1.12.0.0")] +[assembly: AssemblyFileVersion("1.12.0.0")] +[assembly: AssemblyInformationalVersion("1.12.0.0")] // Setting ComVisible to false makes the types in this assembly not visible