Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ TestResults/
!Michaelis_TableOfContents.docx

# Old or generated files to not commit
build_output.txt
wwwroot/sitemap.xml
wwwroot/Chapters
EssentialCSharp.Web/wwwroot/Chapters
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
using Azure.AI.OpenAI;
using Azure.Core;
using Azure.Identity;
using EssentialCSharp.Chat.Common.Models;
using EssentialCSharp.Chat.Common.Services;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.SemanticKernel;
using Npgsql;

Expand Down Expand Up @@ -65,6 +67,15 @@ public static IServiceCollection AddAzureOpenAIServices(
.UseOpenTelemetry();
#pragma warning restore SKEXP0010

// Ensure options are available even when caller provides AIOptions directly.
services.AddOptions<EmbeddingRetryOptions>()
.ValidateDataAnnotations()
.Validate(options =>
{
options.Validate();
return true;
}, "Embedding retry configuration is invalid.");

// Register shared AI services
services.AddSingleton<EmbeddingService>();
services.AddSingleton<AISearchService>();
Expand All @@ -89,6 +100,17 @@ public static IServiceCollection AddAzureOpenAIServices(
// Configure AI options from configuration
services.Configure<AIOptions>(configuration.GetSection("AIOptions"));

// Configure retry options from configuration section
// Environment variables like EmbeddingRetry:MaxRetries will override defaults
services.AddOptions<EmbeddingRetryOptions>()
.Bind(configuration.GetSection(EmbeddingRetryOptions.SectionPath))
.ValidateDataAnnotations()
.Validate(options =>
{
options.Validate();
return true;
}, "Embedding retry configuration is invalid.");

var aiOptions = configuration.GetSection("AIOptions").Get<AIOptions>();
if (aiOptions == null)
{
Expand Down
83 changes: 83 additions & 0 deletions EssentialCSharp.Chat.Shared/Models/EmbeddingRetryOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using System.ComponentModel.DataAnnotations;

namespace EssentialCSharp.Chat.Common.Models;

/// <summary>
/// Configuration options for retry logic when calling external services like Azure OpenAI.
/// </summary>
public sealed class EmbeddingRetryOptions
{
/// <summary>
/// Configuration section path in appsettings.json.
/// </summary>
public const string SectionPath = "AIOptions:EmbeddingRetry";

/// <summary>
/// Maximum number of retries for transient failures.
/// Default is 5 retries (initial attempt + 5 retries = 6 total attempts).
/// </summary>
[Range(0, 20)]
public int MaxRetries { get; set; } = 5;

/// <summary>
/// Base delay in milliseconds before the first retry.
/// Subsequent retries use exponential backoff: baseDelay * (backoffMultiplier ^ attemptNumber).
/// Default is 1000ms (1 second).
/// </summary>
[Range(0, 600000)]
public int BaseDelayMs { get; set; } = 1000;

/// <summary>
/// Maximum delay in milliseconds for exponential backoff before jitter.
/// This caps retry delays to avoid overflow and unbounded waits.
/// </summary>
[Range(1, 600000)]
public int MaxDelayMs { get; set; } = 60000;

/// <summary>
/// Exponential backoff multiplier. Each retry delay is multiplied by this value.
/// For example, with baseDelay=1000ms and multiplier=2.0:
/// - 1st retry: 1000ms
/// - 2nd retry: 2000ms
/// - 3rd retry: 4000ms
/// - 4th retry: 8000ms
/// Default is 2.0 (double each time).
/// </summary>
[Range(1.0, 10.0)]
public double BackoffMultiplier { get; set; } = 2.0;

/// <summary>
/// Maximum jitter fraction added to each retry delay to prevent thundering herd.
/// Jitter is a random value in range [0, maxDelay * maxJitterFraction].
/// For example, with maxJitterFraction=0.2 and delay=1000ms:
/// actual delay will be between 1000ms and 1200ms.
/// Default is 0.2 (20% jitter).
/// </summary>
[Range(0.0, 1.0)]
public double MaxJitterFraction { get; set; } = 0.2;

/// <summary>
/// Validates that configuration values are reasonable.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown if configuration is invalid.</exception>
public void Validate()
{
if (MaxRetries < 0)
throw new InvalidOperationException("MaxRetries must be non-negative.");

if (BaseDelayMs < 0)
throw new InvalidOperationException("BaseDelayMs must be non-negative.");

if (MaxDelayMs <= 0)
throw new InvalidOperationException("MaxDelayMs must be positive.");

if (BaseDelayMs > MaxDelayMs)
throw new InvalidOperationException("BaseDelayMs must be less than or equal to MaxDelayMs.");

if (BackoffMultiplier < 1.0)
throw new InvalidOperationException("BackoffMultiplier must be >= 1.0.");

if (MaxJitterFraction < 0.0 || MaxJitterFraction > 1.0)
throw new InvalidOperationException("MaxJitterFraction must be between 0.0 and 1.0.");
}
}
Loading
Loading