Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
6c21b8f
During Process creation set status as 202 Accepted
Mar 3, 2026
05d5fa5
feat: add Polly retry policy infrastructure (Issue #107)
artcava Mar 3, 2026
228a6be
feat: configure Polly retry policies in appsettings and Program.cs (I…
artcava Mar 3, 2026
1bb5301
test: add unit tests for Polly retry policies (Issue #107)
artcava Mar 3, 2026
a96042e
docs: add comprehensive Polly retry policies documentation (Issue #107)
artcava Mar 3, 2026
9637f59
Fix some errors and dependencies
Mar 3, 2026
3a85b59
fix: add missing NuGet packages and using directives (Issue #107)
artcava Mar 3, 2026
6197a39
fix: resolve package downgrade and AddPolicyHandler error (Issue #107)
artcava Mar 3, 2026
a3d5a28
fix: use Polly v8 compatible HTTP client configuration (Issue #107)
artcava Mar 3, 2026
17a2d7e
fix: resolve MongoDB.Driver version mismatch in StarGate.Api (Issue #…
artcava Mar 3, 2026
33797d2
fix: correct ProjectReference typo in StarGate.Api.csproj
artcava Mar 3, 2026
db6c195
fix: update AspNetCore.HealthChecks.MongoDb to 8.1.0 (Issue #107)
artcava Mar 3, 2026
2debe12
Merge pull request #152 from artcava/feature/107-polly-retry-policies
artcava Mar 3, 2026
3a94f2f
feat: Add CircuitBreakerConfiguration for resilience policies
artcava Mar 3, 2026
58d0eda
feat: Add CircuitBreakerFactory for creating Polly circuit breaker po…
artcava Mar 3, 2026
d1fc8df
feat: Add ResiliencePolicyWrapper to combine retry and circuit breaker
artcava Mar 3, 2026
b8fe3ed
feat: Add CircuitBreakerStateService for tracking circuit states
artcava Mar 3, 2026
55b9cb3
feat: Add CircuitBreakerHealthCheck for monitoring circuit states
artcava Mar 3, 2026
5b8ad34
feat: Update ResilienceServiceCollectionExtensions with circuit break…
artcava Mar 3, 2026
8162c23
feat: Add circuit breaker configuration to appsettings.json
artcava Mar 3, 2026
c2975a4
feat: Register CircuitBreakerStateService and health check in Program.cs
artcava Mar 3, 2026
c9e7898
test: Add comprehensive unit tests for CircuitBreaker implementation
artcava Mar 3, 2026
ca26d01
test: Add unit tests for CircuitBreakerHealthCheck
artcava Mar 3, 2026
a725dc7
docs: Add comprehensive documentation for Circuit Breaker implementation
artcava Mar 3, 2026
7c96678
fix: Increase fail-fast test threshold to handle test environment ove…
artcava Mar 3, 2026
d2f46d8
Merge pull request #153 from artcava/feature/108-implement-polly-circ…
artcava Mar 3, 2026
dc6f304
feat: Add TimeoutConfiguration for timeout policies
artcava Mar 3, 2026
8297627
feat: Add TimeoutPolicyFactory for creating timeout policies
artcava Mar 3, 2026
e98e34a
feat: Add complete resilience policies with timeout integration
artcava Mar 3, 2026
2cf1091
feat: Add timeout configuration to appsettings
artcava Mar 3, 2026
8cae1dd
feat: Register complete resilience policies with timeout support
artcava Mar 3, 2026
d38562b
test: Add comprehensive resilience integration tests
artcava Mar 3, 2026
f55ae07
test: Add chaos testing scenarios for resilience validation
artcava Mar 3, 2026
9380e96
test: Add performance tests for resilience policy overhead
artcava Mar 3, 2026
77f4b29
docs: Add comprehensive resilience strategy documentation
artcava Mar 3, 2026
2f19b36
fix: Correct policy wrapping for HTTP with timeout
artcava Mar 3, 2026
a6494b5
fix: Update DI registration for CompleteHttpResiliencePolicy
artcava Mar 3, 2026
04f140f
fix some errors
Mar 3, 2026
27942de
Merge pull request #154 from artcava/feature/issue-109-timeout-polici…
artcava Mar 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
494 changes: 494 additions & 0 deletions docs/CIRCUIT-BREAKER.md

Large diffs are not rendered by default.

685 changes: 685 additions & 0 deletions docs/POLLY-RETRY-POLICIES.md

Large diffs are not rendered by default.

492 changes: 492 additions & 0 deletions docs/RESILIENCE-STRATEGY.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/StarGate.Api/Endpoints/ProcessEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public static void MapProcessEndpoints(this IEndpointRouteBuilder app)
.WithName("CreateProcess")
.RequireRateLimiting("CreateProcess") // Apply rate limiting policy
.AddValidation<CreateProcessRequest>()
.Produces<ProcessResponse>(StatusCodes.Status201Created)
.Produces<ProcessResponse>(StatusCodes.Status202Accepted)
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status403Forbidden)
Expand Down Expand Up @@ -127,7 +127,7 @@ private static async Task<IResult> CreateProcessAsync(
"Process created successfully: ProcessId={ProcessId}",
process.ProcessId);

return Results.Created($"/api/processes/{process.ProcessId}", response);
return Results.Accepted($"/api/processes/{process.ProcessId}", response);
}
catch (PolicyViolationException ex)
{
Expand Down
6 changes: 3 additions & 3 deletions src/StarGate.Api/StarGate.Api.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="AspNetCore.HealthChecks.MongoDb" Version="8.0.1" />
<PackageReference Include="AspNetCore.HealthChecks.MongoDb" Version="8.1.0" />
<PackageReference Include="AspNetCore.HealthChecks.Rabbitmq" Version="8.0.1" />
<PackageReference Include="AspNetCore.HealthChecks.Redis" Version="8.0.1" />
<PackageReference Include="AspNetCore.HealthChecks.UI.Client" Version="8.0.1" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.9.2" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" />
<PackageReference Include="MongoDB.Driver" Version="2.22.0" />
<PackageReference Include="MongoDB.Driver" Version="2.28.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Polly;
using Polly.Wrap;
using StarGate.Infrastructure.Resilience;

namespace StarGate.Infrastructure.Extensions;

/// <summary>
/// Extension methods for registering resilience policies.
/// </summary>
public static class ResilienceServiceCollectionExtensions
{
/// <summary>
/// Adds resilience policies to the service collection.
/// </summary>
/// <param name="services">The service collection.</param>
/// <param name="configuration">Application configuration.</param>
/// <returns>The service collection for chaining.</returns>
public static IServiceCollection AddResiliencePolicies(
this IServiceCollection services,
IConfiguration configuration)
{
// Register timeout configuration
services.Configure<TimeoutConfiguration>(
configuration.GetSection("Resilience:Timeout"));

// Register retry policy configuration
services.Configure<RetryPolicyConfiguration>(
configuration.GetSection("Resilience:Retry"));

// Register circuit breaker configuration
services.Configure<CircuitBreakerConfiguration>(
configuration.GetSection("Resilience:CircuitBreaker"));

// Register complete wrapped resilience policies (timeout + circuit breaker + retry)
services.AddSingleton(provider =>
{
var timeoutConfig = provider.GetRequiredService<IOptions<TimeoutConfiguration>>().Value;
var retryConfig = provider.GetRequiredService<IOptions<RetryPolicyConfiguration>>().Value;
var circuitConfig = provider.GetRequiredService<IOptions<CircuitBreakerConfiguration>>().Value;
var logger = provider.GetRequiredService<ILogger<RetryPolicyConfiguration>>();
return ResiliencePolicyWrapper.CreateCompleteDatabaseResiliencePolicy(
timeoutConfig, retryConfig, circuitConfig, logger);
});

services.AddSingleton(provider =>
{
var timeoutConfig = provider.GetRequiredService<IOptions<TimeoutConfiguration>>().Value;
var retryConfig = provider.GetRequiredService<IOptions<RetryPolicyConfiguration>>().Value;
var circuitConfig = provider.GetRequiredService<IOptions<CircuitBreakerConfiguration>>().Value;
var logger = provider.GetRequiredService<ILogger<RetryPolicyConfiguration>>();
return ResiliencePolicyWrapper.CreateCompleteBrokerResiliencePolicy(
timeoutConfig, retryConfig, circuitConfig, logger);
});

// Register HTTP complete resilience policy factory as singleton
services.AddSingleton(provider =>
{
var timeoutConfig = provider.GetRequiredService<IOptions<TimeoutConfiguration>>().Value;
var retryConfig = provider.GetRequiredService<IOptions<RetryPolicyConfiguration>>().Value;
var circuitConfig = provider.GetRequiredService<IOptions<CircuitBreakerConfiguration>>().Value;
var loggerFactory = provider.GetRequiredService<ILoggerFactory>();

// Return a factory function that creates HTTP complete resilience policies with appropriate logger
return new Func<ILogger, CompleteHttpResiliencePolicy>(
logger => ResiliencePolicyWrapper.CreateCompleteHttpResiliencePolicy(
timeoutConfig, retryConfig, circuitConfig, logger));
});

return services;
}

/// <summary>
/// Adds HTTP client without automatic resilience policy.
/// Consumers should inject CompleteHttpResiliencePolicy and wrap calls manually.
/// </summary>
/// <typeparam name="TClient">HTTP client interface type.</typeparam>
/// <param name="services">The service collection.</param>
/// <param name="name">HTTP client name.</param>
/// <returns>HTTP client builder for further configuration.</returns>
/// <remarks>
/// To use resilience policies:
/// 1. Inject CompleteHttpResiliencePolicy via factory
/// 2. Wrap HTTP calls: await policy.ExecuteAsync(() => httpClient.SendAsync(request))
/// </remarks>
public static IHttpClientBuilder AddHttpClientWithResilience<TClient>(
this IServiceCollection services,
string name)
where TClient : class
{
return services.AddHttpClient<TClient>(name);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
namespace StarGate.Infrastructure.Resilience;

/// <summary>
/// Configuration for circuit breaker policies.
/// </summary>
public class CircuitBreakerConfiguration
{
/// <summary>
/// Number of consecutive failures before breaking the circuit.
/// </summary>
public int FailureThreshold { get; set; } = 5;

/// <summary>
/// Percentage of failures in sampling duration before breaking.
/// </summary>
public double FailureRateThreshold { get; set; } = 0.5; // 50%

/// <summary>
/// Minimum throughput before considering failure rate.
/// </summary>
public int MinimumThroughput { get; set; } = 10;

/// <summary>
/// Duration to keep circuit open before testing recovery (seconds).
/// </summary>
public double BreakDurationSeconds { get; set; } = 30.0;

/// <summary>
/// Duration to sample for failure rate calculation (seconds).
/// </summary>
public double SamplingDurationSeconds { get; set; } = 60.0;

/// <summary>
/// Gets the break duration as TimeSpan.
/// </summary>
public TimeSpan BreakDuration => TimeSpan.FromSeconds(BreakDurationSeconds);

/// <summary>
/// Gets the sampling duration as TimeSpan.
/// </summary>
public TimeSpan SamplingDuration => TimeSpan.FromSeconds(SamplingDurationSeconds);
}
125 changes: 125 additions & 0 deletions src/StarGate.Infrastructure/Resilience/CircuitBreakerFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
using Microsoft.Extensions.Logging;
using Polly;
using Polly.CircuitBreaker;

namespace StarGate.Infrastructure.Resilience;

/// <summary>
/// Factory for creating Polly circuit breaker policies.
/// </summary>
public static class CircuitBreakerFactory
{
/// <summary>
/// Creates a circuit breaker policy for HTTP operations.
/// </summary>
/// <param name="config">Circuit breaker configuration.</param>
/// <param name="logger">Logger instance.</param>
/// <returns>Configured async circuit breaker policy for HTTP responses.</returns>
public static AsyncCircuitBreakerPolicy<HttpResponseMessage> CreateHttpCircuitBreaker(
CircuitBreakerConfiguration config,
ILogger logger)
{
return Policy
.HandleResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
.Or<HttpRequestException>()
.Or<TimeoutException>()
.AdvancedCircuitBreakerAsync(
failureThreshold: config.FailureRateThreshold,
samplingDuration: config.SamplingDuration,
minimumThroughput: config.MinimumThroughput,
durationOfBreak: config.BreakDuration,
onBreak: (outcome, breakDuration, context) =>
{
var statusCode = outcome.Result?.StatusCode.ToString() ?? "N/A";
var exception = outcome.Exception?.GetType().Name ?? "None";

logger.LogError(
"HTTP circuit breaker opened: StatusCode={StatusCode}, Exception={Exception}, BreakDuration={BreakDuration}s",
statusCode,
exception,
breakDuration.TotalSeconds);
},
onReset: context =>
{
logger.LogInformation("HTTP circuit breaker reset: Circuit closed");
},
onHalfOpen: () =>
{
logger.LogWarning("HTTP circuit breaker half-open: Testing recovery");
});
}

/// <summary>
/// Creates a circuit breaker policy for database operations.
/// </summary>
/// <param name="config">Circuit breaker configuration.</param>
/// <param name="logger">Logger instance.</param>
/// <returns>Configured async circuit breaker policy for database operations.</returns>
public static AsyncCircuitBreakerPolicy CreateDatabaseCircuitBreaker(
CircuitBreakerConfiguration config,
ILogger logger)
{
return Policy
.Handle<TimeoutException>()
.Or<IOException>()
.Or<InvalidOperationException>(ex => ex.Message.Contains("connection", StringComparison.OrdinalIgnoreCase))
.AdvancedCircuitBreakerAsync(
failureThreshold: config.FailureRateThreshold,
samplingDuration: config.SamplingDuration,
minimumThroughput: config.MinimumThroughput,
durationOfBreak: config.BreakDuration,
onBreak: (exception, breakDuration, context) =>
{
logger.LogError(
exception,
"Database circuit breaker opened: Exception={Exception}, BreakDuration={BreakDuration}s",
exception.GetType().Name,
breakDuration.TotalSeconds);
},
onReset: context =>
{
logger.LogInformation("Database circuit breaker reset: Circuit closed");
},
onHalfOpen: () =>
{
logger.LogWarning("Database circuit breaker half-open: Testing recovery");
});
}

/// <summary>
/// Creates a circuit breaker policy for message broker operations.
/// </summary>
/// <param name="config">Circuit breaker configuration.</param>
/// <param name="logger">Logger instance.</param>
/// <returns>Configured async circuit breaker policy for broker operations.</returns>
public static AsyncCircuitBreakerPolicy CreateBrokerCircuitBreaker(
CircuitBreakerConfiguration config,
ILogger logger)
{
return Policy
.Handle<TimeoutException>()
.Or<IOException>()
.Or<InvalidOperationException>(ex => ex.Message.Contains("connection", StringComparison.OrdinalIgnoreCase))
.AdvancedCircuitBreakerAsync(
failureThreshold: config.FailureRateThreshold,
samplingDuration: config.SamplingDuration,
minimumThroughput: config.MinimumThroughput,
durationOfBreak: config.BreakDuration,
onBreak: (exception, breakDuration, context) =>
{
logger.LogError(
exception,
"Broker circuit breaker opened: Exception={Exception}, BreakDuration={BreakDuration}s",
exception.GetType().Name,
breakDuration.TotalSeconds);
},
onReset: context =>
{
logger.LogInformation("Broker circuit breaker reset: Circuit closed");
},
onHalfOpen: () =>
{
logger.LogWarning("Broker circuit breaker half-open: Testing recovery");
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System.Collections.Concurrent;
using Polly.CircuitBreaker;

namespace StarGate.Infrastructure.Resilience;

/// <summary>
/// Service for tracking circuit breaker states.
/// </summary>
public class CircuitBreakerStateService
{
private readonly ConcurrentDictionary<string, CircuitState> _states = new();

/// <summary>
/// Records circuit state change.
/// </summary>
/// <param name="circuitName">Name of the circuit.</param>
/// <param name="state">New state of the circuit.</param>
public void RecordStateChange(string circuitName, CircuitState state)
{
_states.AddOrUpdate(circuitName, state, (_, __) => state);
}

/// <summary>
/// Gets current state of a circuit.
/// </summary>
/// <param name="circuitName">Name of the circuit.</param>
/// <returns>Current state if circuit exists, null otherwise.</returns>
public CircuitState? GetState(string circuitName)
{
return _states.TryGetValue(circuitName, out var state) ? state : null;
}

/// <summary>
/// Gets all circuit states.
/// </summary>
/// <returns>Dictionary of circuit names and their states.</returns>
public Dictionary<string, CircuitState> GetAllStates()
{
return new Dictionary<string, CircuitState>(_states);
}

/// <summary>
/// Checks if any circuit is open.
/// </summary>
/// <returns>True if at least one circuit is open, false otherwise.</returns>
public bool HasOpenCircuit()
{
return _states.Values.Any(state => state == CircuitState.Open);
}
}
Loading
Loading