public async Task ServiceExceptionTest() { var cacheRegion = "cacheRegion"; var serviceInfo = new CalledServiceInfo { Name = "Service", Id = Guid.NewGuid(), Endpoint = "Endpoint", CircuitBreakerInfo = new CircuitBreakerInfo { ExceptionCount = 1, BreakDuration = TimeSpan.FromMilliseconds(1) } }; var cache = Substitute.For <ICache>(); var httpClientWrapper = Substitute.For <IHttpClientWrapper>(); var tokenService = Substitute.For <ITokenService>(); httpClientWrapper.PostAsync(serviceInfo.Endpoint, null, CancellationToken.None).ThrowsForAnyArgs(new Exception()); var sut = new DynamicService(cache, httpClientWrapper, tokenService); ServiceResponse actual = await sut.CallService(serviceInfo, cacheRegion, CancellationToken.None, null); Assert.AreEqual(ServiceResponseStatus.Error, actual.Status); }
public void NoParametersTest() { var cacheRegion = "cacheRegion"; var userIdentifier = "User"; var serviceInfo = new CalledServiceInfo { AdditionalParameters = new ParameterInfo[0] }; var cache = Substitute.For <ICache>(); cache.Get <string>(cacheRegion, Constants.UserIdentifierCacheKey).Returns(new CacheEntry <string> { Value = userIdentifier }); var sut = new CalledServiceBaseInstance(cache); IEnumerable <KeyValuePair <string, string> > actual = sut.GetPostParameters(serviceInfo, cacheRegion, null); cache.Received(1).Get <string>(cacheRegion, Constants.UserIdentifierCacheKey); IEnumerable <KeyValuePair <string, string> > keyValuePairs = actual as KeyValuePair <string, string>[] ?? actual.ToArray(); Assert.IsTrue(keyValuePairs.Any()); Assert.AreEqual(1, keyValuePairs.Count()); KeyValuePair <string, string> parameter = keyValuePairs.First(); Assert.AreEqual(Constants.UserIdentifierPostKey, parameter.Key); Assert.AreEqual(userIdentifier, parameter.Value); }
public async Task ServiceTest() { var serviceInfo = new CalledServiceInfo { Name = "Service", Id = Guid.NewGuid(), Endpoint = "Endpoint", CircuitBreakerInfo = new CircuitBreakerInfo { ExceptionCount = 1, BreakDuration = TimeSpan.FromMilliseconds(1) }, Timeout = TimeSpan.FromMilliseconds(100) }; var httpClientResponse = new HttpClientResponse { HttpStatusCode = HttpStatusCode.NoContent, Response = "" }; var cacheRegion = "cacheRegion"; var cache = Substitute.For <ICache>(); var httpClientWrapper = Substitute.For <IHttpClientWrapper>(); var tokenService = Substitute.For <ITokenService>(); httpClientWrapper.PostAsync(serviceInfo.Endpoint, null, CancellationToken.None).ReturnsForAnyArgs(httpClientResponse); var sut = new CachedService(cache, httpClientWrapper, tokenService); ServiceResponse actual = await sut.CallService(serviceInfo, cacheRegion, CancellationToken.None, null); Assert.AreEqual(ServiceResponseStatus.Success, actual.Status); }
public void AdditionalParametersTest() { var cacheRegion = "cacheRegion"; var userIdentifier = "User"; Guid serviceParameterId = Guid.NewGuid(); Guid nonExistingServiceParameterId = Guid.NewGuid(); var serviceParameterKey = "Key"; var serviceParameterValue = "Value"; var serviceInfo = new CalledServiceInfo { AdditionalParameters = new[] { new ParameterInfo { TokenId = serviceParameterId, Name = serviceParameterKey }, new ParameterInfo { TokenId = nonExistingServiceParameterId, Name = serviceParameterKey } } }; var cache = Substitute.For <ICache>(); cache.Get <string>(cacheRegion, Constants.UserIdentifierCacheKey).Returns(new CacheEntry <string> { Value = userIdentifier }); cache.Get <string>(cacheRegion, serviceParameterId.ToString()).Returns(new CacheEntry <string> { Value = serviceParameterValue }); var sut = new CalledServiceBaseInstance(cache); IEnumerable <KeyValuePair <string, string> > actual = sut.GetPostParameters(serviceInfo, cacheRegion, null); cache.Received(1).Get <string>(cacheRegion, Constants.UserIdentifierCacheKey); IEnumerable <KeyValuePair <string, string> > keyValuePairs = actual as KeyValuePair <string, string>[] ?? actual.ToArray(); Assert.IsTrue(keyValuePairs.Any()); Assert.AreEqual(2, keyValuePairs.Count()); KeyValuePair <string, string> parameter = keyValuePairs.First(); Assert.AreEqual(Constants.UserIdentifierPostKey, parameter.Key); Assert.AreEqual(userIdentifier, parameter.Value); parameter = keyValuePairs.Skip(1).First(); Assert.AreEqual(serviceParameterKey, parameter.Key); Assert.AreEqual(serviceParameterValue, parameter.Value); }
public async Task SemaphoreTest() { var cacheRegion = "cacheRegion"; var serviceInfo = new CalledServiceInfo { Name = "Service", Id = Guid.NewGuid(), Endpoint = "Endpoint", CircuitBreakerInfo = new CircuitBreakerInfo { ExceptionCount = 1, BreakDuration = TimeSpan.FromMilliseconds(1) }, Timeout = TimeSpan.FromMilliseconds(100) }; var httpClientResponse = new HttpClientResponse { HttpStatusCode = HttpStatusCode.NoContent, Response = "" }; var cache = new CacheMock(); var httpClientWrapper = Substitute.For <IHttpClientWrapper>(); var tokenService = Substitute.For <ITokenService>(); httpClientWrapper.PostAsync(serviceInfo.Endpoint, null, CancellationToken.None).ReturnsForAnyArgs(async callInfo => { await Task.Delay(20); return(httpClientResponse); }); var sut = new CachedService(cache, httpClientWrapper, tokenService); Task <ServiceResponse> task1 = Task.Run(async() => await sut.CallService(serviceInfo, cacheRegion, CancellationToken.None, null)); Task <ServiceResponse> task2 = Task.Run(async() => await sut.CallService(serviceInfo, cacheRegion, CancellationToken.None, null)); await Task.WhenAll(task1, task2); await httpClientWrapper.ReceivedWithAnyArgs(1).PostAsync(serviceInfo.Endpoint, null, CancellationToken.None); }
/// <summary> /// Calls the dynamic service and returns the result. /// </summary> /// <param name="serviceInfo">Information about the service to call</param> /// <param name="cacheRegion">The cache region to look for values for post parameters under in</param> /// <param name="cancellationToken">Cancellation token to cancel the request</param> /// <param name="additionalParameters">Additional post parameters to include in the request body</param> /// <returns>A service response representing the result of the call to the dynamic service</returns> protected override async Task <ServiceResponse> CallServiceInternal(CalledServiceInfo serviceInfo, string cacheRegion, CancellationToken cancellationToken, IEnumerable <KeyValuePair <string, string> > additionalParameters) { var serviceResponse = new ServiceResponse { ServiceId = serviceInfo.Id }; try { AsyncPolicyWrap <ServiceResponse> breaker = GetCircuitBreakerPolicy(serviceInfo); return(await breaker.ExecuteAsync(async (cancelToken) => { IEnumerable <KeyValuePair <string, string> > postParameters = GetPostParameters(serviceInfo, cacheRegion, additionalParameters); HttpClientResponse response = await _httpClientWrapper.PostAsync(serviceInfo.Endpoint, postParameters, cancelToken); if (response.HttpStatusCode.IsOkStatus()) { serviceResponse.Value = response.Response; serviceResponse.Status = ServiceResponseStatus.Success; serviceResponse.TokenResponses = _tokenService.ParseTokens(cacheRegion, response.Response, serviceInfo.Tokens).ToArray(); } else { serviceResponse.Status = ServiceResponseStatus.Error; } return serviceResponse; }, cancellationToken : cancellationToken)); } catch (Exception) { serviceResponse.Status = ServiceResponseStatus.Error; return(serviceResponse); } }
/// <summary> /// Generates a list of KeyValuePairs to use as the post body for the request to a called service /// </summary> /// <param name="serviceInfo">Information about the service to generate parameters for</param> /// <param name="cacheRegion">The cache region to look for values for post parameters under in</param> /// <param name="additionalParameters">Additional post parameters to include in the generated list in addition to the ones defined on the service info</param> /// <returns>List of key value pairs to use as post parameters in the body of a request</returns> public IEnumerable <KeyValuePair <string, string> > GetPostParameters(CalledServiceInfo serviceInfo, string cacheRegion, IEnumerable <KeyValuePair <string, string> > additionalParameters) { List <KeyValuePair <string, string> > pairs = additionalParameters?.ToList() ?? new List <KeyValuePair <string, string> >(); CacheEntry <string> userIdentifier = Cache.Get <string>(cacheRegion, Common.Constants.UserIdentifierCacheKey); if (userIdentifier != null) { pairs.Add(new KeyValuePair <string, string>(Common.Constants.UserIdentifierPostKey, userIdentifier.Value)); } if (serviceInfo.AdditionalParameters == null) { return(pairs); } if (!serviceInfo.AdditionalParameters.Any()) { return(pairs); } List <KeyValuePair <string, string> > additionalUserParameters = serviceInfo.AdditionalParameters .Select(additionalParameter => new { additionalParameter, parameterValue = Cache.Get <string>(cacheRegion, additionalParameter.TokenId.ToString())?.Value }) .Where(additionalParameter => !string.IsNullOrEmpty(additionalParameter.parameterValue)) .Select(additionalParameter => new KeyValuePair <string, string>(additionalParameter.additionalParameter.Name, additionalParameter.parameterValue)) .ToList(); pairs.AddRange( additionalUserParameters ); return(pairs); }
/// <summary> /// Generates the circuit breaker policy for a service. /// </summary> /// <param name="serviceInfo">The service to generate the circuit breaker policy for</param> /// <returns>The circuit policy</returns> protected AsyncPolicyWrap <ServiceResponse> GetCircuitBreakerPolicy(CalledServiceInfo serviceInfo) { return(CircuitBreakerPolicies.GetOrAdd(serviceInfo.CacheKey, _ => { AsyncTimeoutPolicy timeoutPolicy = Policy .TimeoutAsync( serviceInfo.Timeout, TimeoutStrategy.Pessimistic ); AsyncCircuitBreakerPolicy <ServiceResponse> circuitBreakerPolicy = Policy <ServiceResponse> .Handle <TimeoutRejectedException>() .OrResult(resultPredicate: serviceResponse => serviceResponse.Status == ServiceResponseStatus.Error) .CircuitBreakerAsync( handledEventsAllowedBeforeBreaking: serviceInfo.CircuitBreakerInfo.ExceptionCount, durationOfBreak: serviceInfo.CircuitBreakerInfo.BreakDuration, onBreak: (__, ___) => { Log.Warning("Service ({ServiceName}) has reached its threshold for the circuit breaker and the circuit has been opened", serviceInfo.Name); }, onReset: () => { Log.Warning("Service ({ServiceName}) has been determined to be back up, circuit closed again", serviceInfo.Name); } ); AsyncPolicyWrap <ServiceResponse> circuitBreakerWrappingTimeout = circuitBreakerPolicy .WrapAsync(timeoutPolicy); AsyncFallbackPolicy <ServiceResponse> timeoutFallbackPolicy = Policy <ServiceResponse> .Handle <TimeoutRejectedException>() .FallbackAsync( cancellationToken => { return Task.FromResult(new ServiceResponse { Status = ServiceResponseStatus.Timeout, ServiceId = serviceInfo.Id }); }); AsyncPolicyWrap <ServiceResponse> timeoutFallbackPolicyWrappingCircuitBreaker = timeoutFallbackPolicy .WrapAsync(circuitBreakerWrappingTimeout); AsyncFallbackPolicy <ServiceResponse> exceptionFallbackPolicy = Policy <ServiceResponse> .Handle <Exception>() .FallbackAsync( cancellationToken => { return Task.FromResult(new ServiceResponse { Status = ServiceResponseStatus.Error, ServiceId = serviceInfo.Id }); }); AsyncPolicyWrap <ServiceResponse> exceptionFallbackPolicyWrappingTimeoutFallback = exceptionFallbackPolicy .WrapAsync(timeoutFallbackPolicyWrappingCircuitBreaker); AsyncPolicyWrap <ServiceResponse> policy = exceptionFallbackPolicyWrappingTimeoutFallback; return policy; })); }
protected abstract Task <ServiceResponse> CallServiceInternal(CalledServiceInfo serviceInfo, string cacheRegion, CancellationToken cancellationToken, IEnumerable <KeyValuePair <string, string> > additionalParameters);
protected override Task <ServiceResponse> CallServiceInternal(CalledServiceInfo serviceInfo, string cacheRegion, CancellationToken cancellationToken, IEnumerable <KeyValuePair <string, string> > additionalParameters) { return(Task.FromResult(new ServiceResponse())); }
/// <summary> /// This method performs the call to the cached service if no value is available in the cache. /// </summary> /// <param name="serviceInfo">Information about the service to call</param> /// <param name="cacheRegion">The cache region to look for an existing response and to look for values for post parameters under in</param> /// <param name="cancellationToken">Cancellation token to cancel the request</param> /// <param name="additionalParameters">Additional post parameters to include in the request body</param> /// <returns>A service response representing the result of the call to the cached service</returns> protected override async Task <ServiceResponse> CallServiceInternal(CalledServiceInfo serviceInfo, string cacheRegion, CancellationToken cancellationToken, IEnumerable <KeyValuePair <string, string> > additionalParameters) { SemaphoreSlim semaphore = _semaphores.GetOrAdd($"{cacheRegion}-{serviceInfo.CacheKey}", _ => new SemaphoreSlim(1, 1)); try { await semaphore.WaitAsync(cancellationToken); if (cancellationToken.IsCancellationRequested) { return(new ServiceResponse { ServiceId = serviceInfo.Id, Status = ServiceResponseStatus.Timeout }); } CacheEntry <string> cacheResult = Cache.Get <string>(cacheRegion, serviceInfo.CacheKey); if (cacheResult != null) { Log.Debug("Read value for service {ServiceName} from cache. Entry has value: {HasValue}", serviceInfo.Name, cacheResult.Value != null); ServiceResponse serviceResponse = new ServiceResponse { ServiceId = serviceInfo.Id, Status = ServiceResponseStatus.Success, Value = cacheResult.Value }; if (string.IsNullOrEmpty(serviceResponse.Value)) { return(serviceResponse); } serviceResponse.TokenResponses = _tokenService.ParseTokens(cacheRegion, cacheResult.Value, serviceInfo.Tokens); foreach (TokenResponse token in serviceResponse.TokenResponses) { Cache.Set(cacheRegion, token.CacheKey, token.Value); } return(serviceResponse); } AsyncPolicyWrap <ServiceResponse> breaker = GetCircuitBreakerPolicy(serviceInfo); return(await breaker.ExecuteAsync(async (cancelToken) => { IEnumerable <KeyValuePair <string, string> > postParameters = GetPostParameters(serviceInfo, cacheRegion, additionalParameters); HttpClientResponse response = await _httpClientWrapper.PostAsync(serviceInfo.Endpoint, postParameters, cancelToken); var serviceResponse = new ServiceResponse { ServiceId = serviceInfo.Id }; if (response.HttpStatusCode.IsOkStatus()) { serviceResponse.Value = response.Response; serviceResponse.Status = ServiceResponseStatus.Success; serviceResponse.TokenResponses = _tokenService.ParseTokens(cacheRegion, response.Response, serviceInfo.Tokens); foreach (TokenResponse token in serviceResponse.TokenResponses) { Cache.Set(cacheRegion, token.CacheKey, token.Value); } Cache.Set(cacheRegion, serviceInfo.CacheKey, response.Response); } else { Cache.Set <string>(cacheRegion, serviceInfo.CacheKey, null); serviceResponse.Status = ServiceResponseStatus.Error; } return serviceResponse; }, cancellationToken : cancellationToken)); } catch (TaskCanceledException) { return(new ServiceResponse { ServiceId = serviceInfo.Id, Status = ServiceResponseStatus.Timeout }); } finally { if (semaphore.CurrentCount == 0) { semaphore.Release(); } } }