diff --git a/README.md b/README.md index 0e26136..3465723 100644 --- a/README.md +++ b/README.md @@ -232,6 +232,7 @@ resource job 'Microsoft.App/jobs@2023-05-01' = { - [Managing periodic tasks in AspNetCore](./samples/AspNetCoreSample) - [Triggering periodic tasks using Tingle.EventBus](./samples/EventBusSample) - [Save executions to a database using Entity Framework](./samples/EFCoreStoreSample) +- [Add retries using Polly's Resilience Pipelines](./samples/ResilienceSample/) ## Issues & Comments diff --git a/Tingle.PeriodicTasks.sln b/Tingle.PeriodicTasks.sln index e61b6e3..ae57c9d 100644 --- a/Tingle.PeriodicTasks.sln +++ b/Tingle.PeriodicTasks.sln @@ -38,6 +38,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EFCoreStoreSample", "sample EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventBusSample", "samples\EventBusSample\EventBusSample.csproj", "{BB2ED193-FA14-4118-8D02-0D5E65FEE996}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ResilienceSample", "samples\ResilienceSample\ResilienceSample.csproj", "{5EAADE1F-4BA5-4E6F-85C0-94773BC3B132}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimpleSample", "samples\SimpleSample\SimpleSample.csproj", "{4EBCCAB0-E459-4771-8510-CE1A5839BC54}" EndProject Global @@ -86,6 +88,10 @@ Global {BB2ED193-FA14-4118-8D02-0D5E65FEE996}.Debug|Any CPU.Build.0 = Debug|Any CPU {BB2ED193-FA14-4118-8D02-0D5E65FEE996}.Release|Any CPU.ActiveCfg = Release|Any CPU {BB2ED193-FA14-4118-8D02-0D5E65FEE996}.Release|Any CPU.Build.0 = Release|Any CPU + {5EAADE1F-4BA5-4E6F-85C0-94773BC3B132}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5EAADE1F-4BA5-4E6F-85C0-94773BC3B132}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5EAADE1F-4BA5-4E6F-85C0-94773BC3B132}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5EAADE1F-4BA5-4E6F-85C0-94773BC3B132}.Release|Any CPU.Build.0 = Release|Any CPU {4EBCCAB0-E459-4771-8510-CE1A5839BC54}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4EBCCAB0-E459-4771-8510-CE1A5839BC54}.Debug|Any CPU.Build.0 = Debug|Any CPU {4EBCCAB0-E459-4771-8510-CE1A5839BC54}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -105,6 +111,7 @@ Global {A44751D3-FACD-4EF4-928A-001075B9D992} = {0ADC51A4-CD37-4D45-8F91-AA493AE00197} {956273BC-3375-4C2B-B1FF-43B05DA98DD0} = {0ADC51A4-CD37-4D45-8F91-AA493AE00197} {BB2ED193-FA14-4118-8D02-0D5E65FEE996} = {0ADC51A4-CD37-4D45-8F91-AA493AE00197} + {5EAADE1F-4BA5-4E6F-85C0-94773BC3B132} = {0ADC51A4-CD37-4D45-8F91-AA493AE00197} {4EBCCAB0-E459-4771-8510-CE1A5839BC54} = {0ADC51A4-CD37-4D45-8F91-AA493AE00197} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/samples/ResilienceSample/Program.cs b/samples/ResilienceSample/Program.cs new file mode 100644 index 0000000..fd3a4d6 --- /dev/null +++ b/samples/ResilienceSample/Program.cs @@ -0,0 +1,58 @@ +using Polly.Retry; +using Polly; +using Tingle.PeriodicTasks; + +var host = Host.CreateDefaultBuilder(args) + .ConfigureServices((context, services) => + { + var environment = context.HostingEnvironment; + var configuration = context.Configuration; + + // register IDistributedLockProvider + var path = configuration.GetValue("DistributedLocking:FilePath") + ?? Path.Combine(environment.ContentRootPath, "distributed-locks"); + services.AddSingleton(provider => + { + return new Medallion.Threading.FileSystem.FileDistributedSynchronizationProvider(Directory.CreateDirectory(path)); + }); + + // register periodic tasks + services.AddPeriodicTasks(builder => + { + builder.AddTask(o => + { + o.Schedule = "*/1 * * * *"; + o.ResiliencePipeline = new ResiliencePipelineBuilder() + .AddRetry(new RetryStrategyOptions + { + ShouldHandle = new PredicateBuilder().Handle(), + Delay = TimeSpan.FromSeconds(1), + MaxRetryAttempts = 3, + BackoffType = DelayBackoffType.Constant, + OnRetry = args => + { + Console.WriteLine($"Attempt {args.AttemptNumber} failed; retrying in {args.RetryDelay}"); + return ValueTask.CompletedTask; + }, + }) + .Build(); + }); + }); + }) + .Build(); + +await host.RunAsync(); + +class DatabaseCleanerTask(ILogger logger) : IPeriodicTask +{ + public async Task ExecuteAsync(PeriodicTaskExecutionContext context, CancellationToken cancellationToken = default) + { + if (Random.Shared.Next(1, 5) > 2) // 60% of the time + { + throw new Exception("Failed to clean up old records from the database"); + } + + logger.LogInformation("Cleaned up old records from the database"); + await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken); + } +} diff --git a/samples/ResilienceSample/Properties/launchSettings.json b/samples/ResilienceSample/Properties/launchSettings.json new file mode 100644 index 0000000..5a3914a --- /dev/null +++ b/samples/ResilienceSample/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "ResilienceSample": { + "commandName": "Project", + "dotnetRunMessages": true, + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} diff --git a/samples/ResilienceSample/ResilienceSample.csproj b/samples/ResilienceSample/ResilienceSample.csproj new file mode 100644 index 0000000..637a246 --- /dev/null +++ b/samples/ResilienceSample/ResilienceSample.csproj @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/samples/ResilienceSample/appsettings.Development.json b/samples/ResilienceSample/appsettings.Development.json new file mode 100644 index 0000000..edadd47 --- /dev/null +++ b/samples/ResilienceSample/appsettings.Development.json @@ -0,0 +1,15 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "Microsoft.Hosting.Lifetime": "Information" + }, + "Console": { + "FormatterName": "simple", + "FormatterOptions": { + "TimestampFormat": "[yyyy-MM-dd HH:mm:ss] ", + "SingleLine": true + } + } + } +} diff --git a/samples/ResilienceSample/appsettings.json b/samples/ResilienceSample/appsettings.json new file mode 100644 index 0000000..b2dcdb6 --- /dev/null +++ b/samples/ResilienceSample/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/src/Tingle.PeriodicTasks/DependencyInjection/PeriodicTaskConfigureOptions.cs b/src/Tingle.PeriodicTasks/DependencyInjection/PeriodicTaskConfigureOptions.cs index beb0eed..877a691 100644 --- a/src/Tingle.PeriodicTasks/DependencyInjection/PeriodicTaskConfigureOptions.cs +++ b/src/Tingle.PeriodicTasks/DependencyInjection/PeriodicTaskConfigureOptions.cs @@ -59,7 +59,7 @@ public void PostConfigure(string? name, PeriodicTaskOptions options) options.LockTimeout ??= tasksHostOptions.DefaultLockTimeout; options.Deadline ??= tasksHostOptions.DefaultDeadline; options.ExecutionIdFormat ??= tasksHostOptions.DefaultExecutionIdFormat; - options.RetryPolicy ??= tasksHostOptions.DefaultRetryPolicy; + options.ResiliencePipeline ??= tasksHostOptions.DefaultResiliencePipeline; } /// diff --git a/src/Tingle.PeriodicTasks/DependencyInjection/PeriodicTasksHostOptions.cs b/src/Tingle.PeriodicTasks/DependencyInjection/PeriodicTasksHostOptions.cs index db3adcd..817a199 100644 --- a/src/Tingle.PeriodicTasks/DependencyInjection/PeriodicTasksHostOptions.cs +++ b/src/Tingle.PeriodicTasks/DependencyInjection/PeriodicTasksHostOptions.cs @@ -22,11 +22,11 @@ public class PeriodicTasksHostOptions public string? LockNamePrefix { get; set; } /// - /// Optional default retry policy to use for periodic tasks where it is not specified. - /// To specify a value per periodic task, use the option. + /// Optional default to use for periodic tasks where it is not specified. + /// To specify a value per periodic task, use the option. /// Defaults to . /// - public AsyncPolicy? DefaultRetryPolicy { get; set; } + public ResiliencePipeline? DefaultResiliencePipeline { get; set; } /// /// Gets or sets the default to use for periodic tasks where it is not specified. diff --git a/src/Tingle.PeriodicTasks/Internal/PeriodicTaskRunner.cs b/src/Tingle.PeriodicTasks/Internal/PeriodicTaskRunner.cs index c205a21..02e4c03 100644 --- a/src/Tingle.PeriodicTasks/Internal/PeriodicTaskRunner.cs +++ b/src/Tingle.PeriodicTasks/Internal/PeriodicTaskRunner.cs @@ -128,12 +128,12 @@ public async Task RunAsync(string name, CancellationToken cancellationToken = de var context = new PeriodicTaskExecutionContext(name, executionId) { TaskType = typeof(TTask), }; - // Invoke handler method, with retry if specified - var retryPolicy = options.RetryPolicy; - if (retryPolicy != null) + // Invoke handler method, with resilience pipeline if specified + var resiliencePipeline = options.ResiliencePipeline; + if (resiliencePipeline != null) { var contextData = new Dictionary { ["context"] = context, }; - await retryPolicy.ExecuteAsync((ctx, ct) => task.ExecuteAsync(context, cts.Token), contextData, cancellationToken).ConfigureAwait(false); + await resiliencePipeline.ExecuteAsync(async (ctx, ct) => await task.ExecuteAsync(context, cts.Token).ConfigureAwait(false), contextData, cancellationToken).ConfigureAwait(false); } else { diff --git a/src/Tingle.PeriodicTasks/PeriodicTaskOptions.cs b/src/Tingle.PeriodicTasks/PeriodicTaskOptions.cs index e973fcf..ca3c0e4 100644 --- a/src/Tingle.PeriodicTasks/PeriodicTaskOptions.cs +++ b/src/Tingle.PeriodicTasks/PeriodicTaskOptions.cs @@ -88,7 +88,7 @@ public class PeriodicTaskOptions public string? LockName { get; set; } /// - /// The retry policy to apply when executing the job. + /// The to apply when executing the job. /// This is an outer wrapper around the /// /// method. @@ -98,7 +98,7 @@ public class PeriodicTaskOptions /// /// /// When a value is provided, the host may extend the duration of the distributed for the task - /// until the execution with retry policy completes successfully or not. + /// until the execution with this pipeline completes successfully or not. /// - public AsyncPolicy? RetryPolicy { get; set; } + public ResiliencePipeline? ResiliencePipeline { get; set; } } diff --git a/src/Tingle.PeriodicTasks/Tingle.PeriodicTasks.csproj b/src/Tingle.PeriodicTasks/Tingle.PeriodicTasks.csproj index 90637c1..2904a52 100644 --- a/src/Tingle.PeriodicTasks/Tingle.PeriodicTasks.csproj +++ b/src/Tingle.PeriodicTasks/Tingle.PeriodicTasks.csproj @@ -16,7 +16,7 @@ - +