diff --git a/samples/Foundatio.HostingSample/Jobs/EvenMinutesJob.cs b/samples/Foundatio.HostingSample/Jobs/EvenMinutesJob.cs index bb33d7507..3f3958e68 100644 --- a/samples/Foundatio.HostingSample/Jobs/EvenMinutesJob.cs +++ b/samples/Foundatio.HostingSample/Jobs/EvenMinutesJob.cs @@ -4,25 +4,24 @@ using Foundatio.Jobs; using Microsoft.Extensions.Logging; -namespace Foundatio.HostingSample +namespace Foundatio.HostingSample; + +public class EvenMinutesJob : IJob { - public class EvenMinutesJob : IJob - { - private readonly ILogger _logger; + private readonly ILogger _logger; - public EvenMinutesJob(ILoggerFactory loggerFactory) - { - _logger = loggerFactory.CreateLogger(); - } + public EvenMinutesJob(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } - public async Task RunAsync(CancellationToken cancellationToken = default) - { - if (_logger.IsEnabled(LogLevel.Information)) - _logger.LogInformation("EvenMinuteJob Run Thread={ManagedThreadId}", Thread.CurrentThread.ManagedThreadId); + public async Task RunAsync(CancellationToken cancellationToken = default) + { + if (_logger.IsEnabled(LogLevel.Information)) + _logger.LogInformation("EvenMinuteJob Run Thread={ManagedThreadId}", Thread.CurrentThread.ManagedThreadId); - await Task.Delay(TimeSpan.FromSeconds(5)); + await Task.Delay(TimeSpan.FromSeconds(5)); - return JobResult.Success; - } + return JobResult.Success; } } diff --git a/samples/Foundatio.HostingSample/Jobs/EveryMinuteJob.cs b/samples/Foundatio.HostingSample/Jobs/EveryMinuteJob.cs index 2eeb323c8..3f697a939 100644 --- a/samples/Foundatio.HostingSample/Jobs/EveryMinuteJob.cs +++ b/samples/Foundatio.HostingSample/Jobs/EveryMinuteJob.cs @@ -4,25 +4,24 @@ using Foundatio.Jobs; using Microsoft.Extensions.Logging; -namespace Foundatio.HostingSample +namespace Foundatio.HostingSample; + +public class EveryMinuteJob : IJob { - public class EveryMinuteJob : IJob - { - private readonly ILogger _logger; + private readonly ILogger _logger; - public EveryMinuteJob(ILoggerFactory loggerFactory) - { - _logger = loggerFactory.CreateLogger(); - } + public EveryMinuteJob(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } - public async Task RunAsync(CancellationToken cancellationToken = default) - { - if (_logger.IsEnabled(LogLevel.Information)) - _logger.LogInformation("EveryMinuteJob Run Thread={ManagedThreadId}", Thread.CurrentThread.ManagedThreadId); + public async Task RunAsync(CancellationToken cancellationToken = default) + { + if (_logger.IsEnabled(LogLevel.Information)) + _logger.LogInformation("EveryMinuteJob Run Thread={ManagedThreadId}", Thread.CurrentThread.ManagedThreadId); - await Task.Delay(TimeSpan.FromSeconds(4)); + await Task.Delay(TimeSpan.FromSeconds(4)); - return JobResult.Success; - } + return JobResult.Success; } } diff --git a/samples/Foundatio.HostingSample/Jobs/Sample1Job.cs b/samples/Foundatio.HostingSample/Jobs/Sample1Job.cs index 561a0cfab..f8f09af7c 100644 --- a/samples/Foundatio.HostingSample/Jobs/Sample1Job.cs +++ b/samples/Foundatio.HostingSample/Jobs/Sample1Job.cs @@ -3,26 +3,25 @@ using Foundatio.Jobs; using Microsoft.Extensions.Logging; -namespace Foundatio.HostingSample +namespace Foundatio.HostingSample; + +[Job(Description = "Sample 1 job", Interval = "5s", IterationLimit = 5)] +public class Sample1Job : IJob { - [Job(Description = "Sample 1 job", Interval = "5s", IterationLimit = 5)] - public class Sample1Job : IJob - { - private readonly ILogger _logger; - private int _iterationCount = 0; + private readonly ILogger _logger; + private int _iterationCount = 0; - public Sample1Job(ILoggerFactory loggerFactory) - { - _logger = loggerFactory.CreateLogger(); - } + public Sample1Job(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } - public Task RunAsync(CancellationToken cancellationToken = default) - { - Interlocked.Increment(ref _iterationCount); - if (_logger.IsEnabled(LogLevel.Information)) - _logger.LogTrace("Sample1Job Run #{IterationCount} Thread={ManagedThreadId}", _iterationCount, Thread.CurrentThread.ManagedThreadId); + public Task RunAsync(CancellationToken cancellationToken = default) + { + Interlocked.Increment(ref _iterationCount); + if (_logger.IsEnabled(LogLevel.Information)) + _logger.LogTrace("Sample1Job Run #{IterationCount} Thread={ManagedThreadId}", _iterationCount, Thread.CurrentThread.ManagedThreadId); - return Task.FromResult(JobResult.Success); - } + return Task.FromResult(JobResult.Success); } } diff --git a/samples/Foundatio.HostingSample/Jobs/Sample2Job.cs b/samples/Foundatio.HostingSample/Jobs/Sample2Job.cs index 5b519b345..df6e44aa2 100644 --- a/samples/Foundatio.HostingSample/Jobs/Sample2Job.cs +++ b/samples/Foundatio.HostingSample/Jobs/Sample2Job.cs @@ -6,39 +6,38 @@ using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Logging; -namespace Foundatio.HostingSample +namespace Foundatio.HostingSample; + +[Job(Description = "Sample 2 job", Interval = "2s", IterationLimit = 10)] +public class Sample2Job : IJob, IHealthCheck { - [Job(Description = "Sample 2 job", Interval = "2s", IterationLimit = 10)] - public class Sample2Job : IJob, IHealthCheck + private readonly ILogger _logger; + private int _iterationCount = 0; + private DateTime? _lastRun = null; + + public Sample2Job(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + + public Task RunAsync(CancellationToken cancellationToken = default) { - private readonly ILogger _logger; - private int _iterationCount = 0; - private DateTime? _lastRun = null; - - public Sample2Job(ILoggerFactory loggerFactory) - { - _logger = loggerFactory.CreateLogger(); - } - - public Task RunAsync(CancellationToken cancellationToken = default) - { - _lastRun = SystemClock.UtcNow; - Interlocked.Increment(ref _iterationCount); - if (_logger.IsEnabled(LogLevel.Information)) - _logger.LogTrace("Sample2Job Run #{IterationCount} Thread={ManagedThreadId}", _iterationCount, Thread.CurrentThread.ManagedThreadId); - - return Task.FromResult(JobResult.Success); - } - - public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) - { - if (!_lastRun.HasValue) - return Task.FromResult(HealthCheckResult.Healthy("Job has not been run yet.")); - - if (SystemClock.UtcNow.Subtract(_lastRun.Value) > TimeSpan.FromSeconds(5)) - return Task.FromResult(HealthCheckResult.Unhealthy("Job has not run in the last 5 seconds.")); - - return Task.FromResult(HealthCheckResult.Healthy("Job has run in the last 5 seconds.")); - } + _lastRun = SystemClock.UtcNow; + Interlocked.Increment(ref _iterationCount); + if (_logger.IsEnabled(LogLevel.Information)) + _logger.LogTrace("Sample2Job Run #{IterationCount} Thread={ManagedThreadId}", _iterationCount, Thread.CurrentThread.ManagedThreadId); + + return Task.FromResult(JobResult.Success); + } + + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + if (!_lastRun.HasValue) + return Task.FromResult(HealthCheckResult.Healthy("Job has not been run yet.")); + + if (SystemClock.UtcNow.Subtract(_lastRun.Value) > TimeSpan.FromSeconds(5)) + return Task.FromResult(HealthCheckResult.Unhealthy("Job has not run in the last 5 seconds.")); + + return Task.FromResult(HealthCheckResult.Healthy("Job has run in the last 5 seconds.")); } } diff --git a/samples/Foundatio.HostingSample/MyCriticalHealthCheck.cs b/samples/Foundatio.HostingSample/MyCriticalHealthCheck.cs index 60707a73d..7d3d7ac84 100644 --- a/samples/Foundatio.HostingSample/MyCriticalHealthCheck.cs +++ b/samples/Foundatio.HostingSample/MyCriticalHealthCheck.cs @@ -3,17 +3,16 @@ using System.Threading.Tasks; using Microsoft.Extensions.Diagnostics.HealthChecks; -namespace Foundatio.HostingSample +namespace Foundatio.HostingSample; + +public class MyCriticalHealthCheck : IHealthCheck { - public class MyCriticalHealthCheck : IHealthCheck - { - private static DateTime _startTime = DateTime.Now; + private static DateTime _startTime = DateTime.Now; - public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = new CancellationToken()) - { - return DateTime.Now.Subtract(_startTime) > TimeSpan.FromSeconds(3) ? - Task.FromResult(HealthCheckResult.Healthy("Critical resource is available.")) - : Task.FromResult(HealthCheckResult.Unhealthy("Critical resource not available.")); - } + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = new CancellationToken()) + { + return DateTime.Now.Subtract(_startTime) > TimeSpan.FromSeconds(3) ? + Task.FromResult(HealthCheckResult.Healthy("Critical resource is available.")) + : Task.FromResult(HealthCheckResult.Unhealthy("Critical resource not available.")); } } diff --git a/samples/Foundatio.HostingSample/Program.cs b/samples/Foundatio.HostingSample/Program.cs index 1aebd349f..a1e675989 100644 --- a/samples/Foundatio.HostingSample/Program.cs +++ b/samples/Foundatio.HostingSample/Program.cs @@ -12,130 +12,129 @@ using Serilog; using Serilog.Events; -namespace Foundatio.HostingSample +namespace Foundatio.HostingSample; + +public class Program { - public class Program + public static int Main(string[] args) { - public static int Main(string[] args) + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Verbose() + .MinimumLevel.Override("Microsoft", LogEventLevel.Information) + .MinimumLevel.Override("Microsoft.AspNetCore.Hosting.Diagnostics", LogEventLevel.Warning) + .Enrich.FromLogContext() + .WriteTo.Console() + .CreateLogger(); + + try { - Log.Logger = new LoggerConfiguration() - .MinimumLevel.Verbose() - .MinimumLevel.Override("Microsoft", LogEventLevel.Information) - .MinimumLevel.Override("Microsoft.AspNetCore.Hosting.Diagnostics", LogEventLevel.Warning) - .Enrich.FromLogContext() - .WriteTo.Console() - .CreateLogger(); - - try - { - Log.Information("Starting host"); - CreateHostBuilder(args).Build().Run(); - return 0; - } - catch (Exception ex) - { - Log.Fatal(ex, "Host terminated unexpectedly"); - return 1; - } - finally - { - Log.CloseAndFlush(); + Log.Information("Starting host"); + CreateHostBuilder(args).Build().Run(); + return 0; + } + catch (Exception ex) + { + Log.Fatal(ex, "Host terminated unexpectedly"); + return 1; + } + finally + { + Log.CloseAndFlush(); - if (Debugger.IsAttached) - Console.ReadKey(); - } + if (Debugger.IsAttached) + Console.ReadKey(); } + } - public static IHostBuilder CreateHostBuilder(string[] args) - { - bool all = args.Contains("all", StringComparer.OrdinalIgnoreCase); - bool sample1 = all || args.Contains("sample1", StringComparer.OrdinalIgnoreCase); - bool sample2 = all || args.Contains("sample2", StringComparer.OrdinalIgnoreCase); - bool everyMinute = all || args.Contains("everyMinute", StringComparer.OrdinalIgnoreCase); - bool evenMinutes = all || args.Contains("evenMinutes", StringComparer.OrdinalIgnoreCase); - - var builder = Host.CreateDefaultBuilder(args) - .UseSerilog() - .ConfigureWebHostDefaults(webBuilder => + public static IHostBuilder CreateHostBuilder(string[] args) + { + bool all = args.Contains("all", StringComparer.OrdinalIgnoreCase); + bool sample1 = all || args.Contains("sample1", StringComparer.OrdinalIgnoreCase); + bool sample2 = all || args.Contains("sample2", StringComparer.OrdinalIgnoreCase); + bool everyMinute = all || args.Contains("everyMinute", StringComparer.OrdinalIgnoreCase); + bool evenMinutes = all || args.Contains("evenMinutes", StringComparer.OrdinalIgnoreCase); + + var builder = Host.CreateDefaultBuilder(args) + .UseSerilog() + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.Configure(app => { - webBuilder.Configure(app => - { - app.UseSerilogRequestLogging(); + app.UseSerilogRequestLogging(); - app.UseHealthChecks("/health"); - app.UseReadyHealthChecks("Critical"); + app.UseHealthChecks("/health"); + app.UseReadyHealthChecks("Critical"); - // this middleware will return Service Unavailable until the startup actions have completed - app.UseWaitForStartupActionsBeforeServingRequests(); + // this middleware will return Service Unavailable until the startup actions have completed + app.UseWaitForStartupActionsBeforeServingRequests(); - // add mvc or other request middleware after the UseWaitForStartupActionsBeforeServingRequests call - }); - }) - .ConfigureServices(s => - { - // will shutdown the host if no jobs are running - s.AddJobLifetimeService(); + // add mvc or other request middleware after the UseWaitForStartupActionsBeforeServingRequests call + }); + }) + .ConfigureServices(s => + { + // will shutdown the host if no jobs are running + s.AddJobLifetimeService(); - // inserts a startup action that does not complete until the critical health checks are healthy - // gets inserted as 1st startup action so that any other startup actions dont run until the critical resources are available - s.AddStartupActionToWaitForHealthChecks("Critical"); + // inserts a startup action that does not complete until the critical health checks are healthy + // gets inserted as 1st startup action so that any other startup actions dont run until the critical resources are available + s.AddStartupActionToWaitForHealthChecks("Critical"); - s.AddHealthChecks().AddCheck("My Critical Resource", tags: new[] { "Critical" }); + s.AddHealthChecks().AddCheck("My Critical Resource", tags: new[] { "Critical" }); - // add health check that does not return healthy until the startup actions have completed - // useful for readiness checks - s.AddHealthChecks().AddCheckForStartupActions("Critical"); + // add health check that does not return healthy until the startup actions have completed + // useful for readiness checks + s.AddHealthChecks().AddCheckForStartupActions("Critical"); - if (everyMinute) - s.AddCronJob("* * * * *"); + if (everyMinute) + s.AddCronJob("* * * * *"); - if (evenMinutes) - s.AddCronJob("*/2 * * * *"); + if (evenMinutes) + s.AddCronJob("*/2 * * * *"); - if (sample1) - s.AddJob(sp => new Sample1Job(sp.GetRequiredService()), o => o.ApplyDefaults().WaitForStartupActions(true).InitialDelay(TimeSpan.FromSeconds(4))); + if (sample1) + s.AddJob(sp => new Sample1Job(sp.GetRequiredService()), o => o.ApplyDefaults().WaitForStartupActions(true).InitialDelay(TimeSpan.FromSeconds(4))); + + if (sample2) + { + s.AddHealthChecks().AddCheck("Sample2Job"); + s.AddJob(true); + } - if (sample2) + // if you don't specify priority, actions will automatically be assigned an incrementing priority starting at 0 + s.AddStartupAction("Test1", async sp => + { + var logger = sp.GetRequiredService>(); + logger.LogTrace("Running startup 1 action"); + for (int i = 0; i < 3; i++) { - s.AddHealthChecks().AddCheck("Sample2Job"); - s.AddJob(true); + await Task.Delay(1000); + logger.LogTrace("Running startup 1 action..."); } - // if you don't specify priority, actions will automatically be assigned an incrementing priority starting at 0 - s.AddStartupAction("Test1", async sp => - { - var logger = sp.GetRequiredService>(); - logger.LogTrace("Running startup 1 action"); - for (int i = 0; i < 3; i++) - { - await Task.Delay(1000); - logger.LogTrace("Running startup 1 action..."); - } - - logger.LogTrace("Done running startup 1 action"); - }); - - // then these startup actions will run concurrently since they both have the same priority - s.AddStartupAction(priority: 100); - s.AddStartupAction(priority: 100); - - s.AddStartupAction("Test2", async sp => + logger.LogTrace("Done running startup 1 action"); + }); + + // then these startup actions will run concurrently since they both have the same priority + s.AddStartupAction(priority: 100); + s.AddStartupAction(priority: 100); + + s.AddStartupAction("Test2", async sp => + { + var logger = sp.GetRequiredService>(); + logger.LogTrace("Running startup 2 action"); + for (int i = 0; i < 2; i++) { - var logger = sp.GetRequiredService>(); - logger.LogTrace("Running startup 2 action"); - for (int i = 0; i < 2; i++) - { - await Task.Delay(1500); - logger.LogTrace("Running startup 2 action..."); - } - //throw new ApplicationException("Boom goes the startup"); - logger.LogTrace("Done running startup 2 action"); - }); - - //s.AddStartupAction("Boom", () => throw new ApplicationException("Boom goes the startup")); + await Task.Delay(1500); + logger.LogTrace("Running startup 2 action..."); + } + //throw new ApplicationException("Boom goes the startup"); + logger.LogTrace("Done running startup 2 action"); }); - return builder; - } + //s.AddStartupAction("Boom", () => throw new ApplicationException("Boom goes the startup")); + }); + + return builder; } } diff --git a/samples/Foundatio.HostingSample/Startup/MyStartupAction.cs b/samples/Foundatio.HostingSample/Startup/MyStartupAction.cs index 7eee41247..12c891378 100644 --- a/samples/Foundatio.HostingSample/Startup/MyStartupAction.cs +++ b/samples/Foundatio.HostingSample/Startup/MyStartupAction.cs @@ -3,24 +3,23 @@ using Foundatio.Extensions.Hosting.Startup; using Microsoft.Extensions.Logging; -namespace Foundatio.HostingSample +namespace Foundatio.HostingSample; + +public class MyStartupAction : IStartupAction { - public class MyStartupAction : IStartupAction - { - private readonly ILogger _logger; + private readonly ILogger _logger; - public MyStartupAction(ILogger logger) - { - _logger = logger; - } + public MyStartupAction(ILogger logger) + { + _logger = logger; + } - public async Task RunAsync(CancellationToken cancellationToken = default) + public async Task RunAsync(CancellationToken cancellationToken = default) + { + for (int i = 0; i < 5; i++) { - for (int i = 0; i < 5; i++) - { - _logger.LogTrace("MyStartupAction Run Thread={ManagedThreadId}", Thread.CurrentThread.ManagedThreadId); - await Task.Delay(500); - } + _logger.LogTrace("MyStartupAction Run Thread={ManagedThreadId}", Thread.CurrentThread.ManagedThreadId); + await Task.Delay(500); } } } diff --git a/samples/Foundatio.HostingSample/Startup/OtherStartupAction.cs b/samples/Foundatio.HostingSample/Startup/OtherStartupAction.cs index 0a17292f7..96cce9a33 100644 --- a/samples/Foundatio.HostingSample/Startup/OtherStartupAction.cs +++ b/samples/Foundatio.HostingSample/Startup/OtherStartupAction.cs @@ -3,24 +3,23 @@ using Foundatio.Extensions.Hosting.Startup; using Microsoft.Extensions.Logging; -namespace Foundatio.HostingSample +namespace Foundatio.HostingSample; + +public class OtherStartupAction : IStartupAction { - public class OtherStartupAction : IStartupAction - { - private readonly ILogger _logger; + private readonly ILogger _logger; - public OtherStartupAction(ILogger logger) - { - _logger = logger; - } + public OtherStartupAction(ILogger logger) + { + _logger = logger; + } - public async Task RunAsync(CancellationToken cancellationToken = default) + public async Task RunAsync(CancellationToken cancellationToken = default) + { + for (int i = 0; i < 5; i++) { - for (int i = 0; i < 5; i++) - { - _logger.LogTrace("OtherStartupAction Run Thread={ManagedThreadId}", Thread.CurrentThread.ManagedThreadId); - await Task.Delay(900); - } + _logger.LogTrace("OtherStartupAction Run Thread={ManagedThreadId}", Thread.CurrentThread.ManagedThreadId); + await Task.Delay(900); } } } diff --git a/src/Foundatio.AppMetrics/AppMetricsClient.cs b/src/Foundatio.AppMetrics/AppMetricsClient.cs index 83df016e9..6f0072004 100644 --- a/src/Foundatio.AppMetrics/AppMetricsClient.cs +++ b/src/Foundatio.AppMetrics/AppMetricsClient.cs @@ -4,32 +4,31 @@ using App.Metrics.Gauge; using App.Metrics.Timer; -namespace Foundatio.Metrics -{ - public class AppMetricsClient : IMetricsClient - { - private readonly IMetrics _metrics; +namespace Foundatio.Metrics; - public AppMetricsClient(IMetrics metrics) - { - _metrics = metrics; - } +public class AppMetricsClient : IMetricsClient +{ + private readonly IMetrics _metrics; - public void Counter(string name, int value = 1) - { - _metrics.Provider.Counter.Instance(new CounterOptions { Name = name }).Increment(value); - } + public AppMetricsClient(IMetrics metrics) + { + _metrics = metrics; + } - public void Gauge(string name, double value) - { - _metrics.Provider.Gauge.Instance(new GaugeOptions { Name = name }).SetValue(value); - } + public void Counter(string name, int value = 1) + { + _metrics.Provider.Counter.Instance(new CounterOptions { Name = name }).Increment(value); + } - public void Timer(string name, int milliseconds) - { - _metrics.Provider.Timer.Instance(new TimerOptions { Name = name }).Record(milliseconds, TimeUnit.Milliseconds); - } + public void Gauge(string name, double value) + { + _metrics.Provider.Gauge.Instance(new GaugeOptions { Name = name }).SetValue(value); + } - public void Dispose() { } + public void Timer(string name, int milliseconds) + { + _metrics.Provider.Timer.Instance(new TimerOptions { Name = name }).Record(milliseconds, TimeUnit.Milliseconds); } + + public void Dispose() { } } diff --git a/src/Foundatio.DataProtection/Extensions/DataProtectionBuilderExtensions.cs b/src/Foundatio.DataProtection/Extensions/DataProtectionBuilderExtensions.cs index 45a06890d..3e1976c95 100644 --- a/src/Foundatio.DataProtection/Extensions/DataProtectionBuilderExtensions.cs +++ b/src/Foundatio.DataProtection/Extensions/DataProtectionBuilderExtensions.cs @@ -6,76 +6,75 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -namespace Foundatio.DataProtection +namespace Foundatio.DataProtection; + +public static class DataProtectionBuilderExtensions { - public static class DataProtectionBuilderExtensions + /// + /// Configures the data protection system to persist keys to file storage. + /// + /// The builder instance to modify. + /// The storage account to use. + /// The logger factory to use. + /// The value . + public static IDataProtectionBuilder PersistKeysToFileStorage(this IDataProtectionBuilder builder, IFileStorage storage, ILoggerFactory loggerFactory = null) { - /// - /// Configures the data protection system to persist keys to file storage. - /// - /// The builder instance to modify. - /// The storage account to use. - /// The logger factory to use. - /// The value . - public static IDataProtectionBuilder PersistKeysToFileStorage(this IDataProtectionBuilder builder, IFileStorage storage, ILoggerFactory loggerFactory = null) - { - if (builder == null) - throw new ArgumentNullException(nameof(builder)); + if (builder == null) + throw new ArgumentNullException(nameof(builder)); - if (storage == null) - throw new ArgumentNullException(nameof(storage)); + if (storage == null) + throw new ArgumentNullException(nameof(storage)); - builder.Services.Configure(options => - { - options.XmlRepository = new FoundatioStorageXmlRepository(storage, loggerFactory); - }); + builder.Services.Configure(options => + { + options.XmlRepository = new FoundatioStorageXmlRepository(storage, loggerFactory); + }); - return builder; - } + return builder; + } - /// - /// Configures the data protection system to persist keys to file storage. - /// - /// The builder instance to modify. - /// The storage factory to use. - /// The value . - public static IDataProtectionBuilder PersistKeysToFileStorage(this IDataProtectionBuilder builder, Func storageFactory) + /// + /// Configures the data protection system to persist keys to file storage. + /// + /// The builder instance to modify. + /// The storage factory to use. + /// The value . + public static IDataProtectionBuilder PersistKeysToFileStorage(this IDataProtectionBuilder builder, Func storageFactory) + { + if (builder == null) + throw new ArgumentNullException(nameof(builder)); + + builder.Services.AddSingleton>(services => { - if (builder == null) - throw new ArgumentNullException(nameof(builder)); + var storage = storageFactory?.Invoke(services); + if (storage == null) + throw new ArgumentNullException(nameof(storageFactory)); - builder.Services.AddSingleton>(services => - { - var storage = storageFactory?.Invoke(services); - if (storage == null) - throw new ArgumentNullException(nameof(storageFactory)); + var loggerFactory = services.GetService(); + return new ConfigureOptions(options => options.XmlRepository = new FoundatioStorageXmlRepository(storage, loggerFactory)); + }); - var loggerFactory = services.GetService(); - return new ConfigureOptions(options => options.XmlRepository = new FoundatioStorageXmlRepository(storage, loggerFactory)); - }); + return builder; + } - return builder; - } + /// + /// Configures the data protection system to persist keys to file storage. + /// + /// The builder instance to modify. + /// The value . + public static IDataProtectionBuilder PersistKeysToFileStorage(this IDataProtectionBuilder builder) + { + if (builder == null) + throw new ArgumentNullException(nameof(builder)); - /// - /// Configures the data protection system to persist keys to file storage. - /// - /// The builder instance to modify. - /// The value . - public static IDataProtectionBuilder PersistKeysToFileStorage(this IDataProtectionBuilder builder) + builder.Services.AddSingleton>(services => { - if (builder == null) - throw new ArgumentNullException(nameof(builder)); - - builder.Services.AddSingleton>(services => - { - var storage = services.GetRequiredService(); - var loggerFactory = services.GetService(); + var storage = services.GetRequiredService(); + var loggerFactory = services.GetService(); - return new ConfigureOptions(options => options.XmlRepository = new FoundatioStorageXmlRepository(storage, loggerFactory)); - }); + return new ConfigureOptions(options => options.XmlRepository = new FoundatioStorageXmlRepository(storage, loggerFactory)); + }); - return builder; - } + return builder; } } diff --git a/src/Foundatio.DataProtection/FoundatioStorageXmlRepository.cs b/src/Foundatio.DataProtection/FoundatioStorageXmlRepository.cs index 2e2481e43..fa2871c7c 100644 --- a/src/Foundatio.DataProtection/FoundatioStorageXmlRepository.cs +++ b/src/Foundatio.DataProtection/FoundatioStorageXmlRepository.cs @@ -10,86 +10,85 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -namespace Foundatio.DataProtection +namespace Foundatio.DataProtection; + +/// +/// An which is backed by Foundatio Storage. +/// +/// +/// Instances of this type are thread-safe. +/// +public sealed class FoundatioStorageXmlRepository : IXmlRepository { + private readonly IFileStorage _storage; + private readonly ILogger _logger; + /// - /// An which is backed by Foundatio Storage. + /// Creates a new instance of the . /// - /// - /// Instances of this type are thread-safe. - /// - public sealed class FoundatioStorageXmlRepository : IXmlRepository + public FoundatioStorageXmlRepository(IFileStorage storage, ILoggerFactory loggerFactory = null) { - private readonly IFileStorage _storage; - private readonly ILogger _logger; + if (storage == null) + throw new ArgumentNullException(nameof(storage)); - /// - /// Creates a new instance of the . - /// - public FoundatioStorageXmlRepository(IFileStorage storage, ILoggerFactory loggerFactory = null) - { - if (storage == null) - throw new ArgumentNullException(nameof(storage)); + _storage = new ScopedFileStorage(storage, "DataProtection"); + _logger = loggerFactory?.CreateLogger() ?? NullLogger.Instance; + } - _storage = new ScopedFileStorage(storage, "DataProtection"); - _logger = loggerFactory?.CreateLogger() ?? NullLogger.Instance; - } + /// + public IReadOnlyCollection GetAllElements() + { + return GetAllElementsAsync().GetAwaiter().GetResult(); + } - /// - public IReadOnlyCollection GetAllElements() + private async Task> GetAllElementsAsync() + { + _logger.LogTrace("Loading elements..."); + var files = (await _storage.GetFileListAsync("*.xml").AnyContext()).ToList(); + if (files.Count == 0) { - return GetAllElementsAsync().GetAwaiter().GetResult(); + _logger.LogTrace("No elements were found"); + return new XElement[0]; } - private async Task> GetAllElementsAsync() + _logger.LogTrace("Found {FileCount} elements.", files.Count); + var elements = new List(files.Count); + foreach (var file in files) { - _logger.LogTrace("Loading elements..."); - var files = (await _storage.GetFileListAsync("*.xml").AnyContext()).ToList(); - if (files.Count == 0) + _logger.LogTrace("Loading element: {File}", file.Path); + using (var stream = await _storage.GetFileStreamAsync(file.Path).AnyContext()) { - _logger.LogTrace("No elements were found"); - return new XElement[0]; + elements.Add(XElement.Load(stream)); } - _logger.LogTrace("Found {FileCount} elements.", files.Count); - var elements = new List(files.Count); - foreach (var file in files) - { - _logger.LogTrace("Loading element: {File}", file.Path); - using (var stream = await _storage.GetFileStreamAsync(file.Path).AnyContext()) - { - elements.Add(XElement.Load(stream)); - } + _logger.LogTrace("Loaded element: {File}", file.Path); + } - _logger.LogTrace("Loaded element: {File}", file.Path); - } + return elements.AsReadOnly(); + } - return elements.AsReadOnly(); - } + /// + public void StoreElement(XElement element, string friendlyName) + { + if (element == null) + throw new ArgumentNullException(nameof(element)); - /// - public void StoreElement(XElement element, string friendlyName) - { - if (element == null) - throw new ArgumentNullException(nameof(element)); + StoreElementAsync(element, friendlyName).GetAwaiter().GetResult(); + } - StoreElementAsync(element, friendlyName).GetAwaiter().GetResult(); - } + private Task StoreElementAsync(XElement element, string friendlyName) + { + string path = String.Concat(!String.IsNullOrEmpty(friendlyName) ? friendlyName : Guid.NewGuid().ToString("N"), ".xml"); + _logger.LogTrace("Saving element: {File}.", path); - private Task StoreElementAsync(XElement element, string friendlyName) + return Run.WithRetriesAsync(async () => { - string path = String.Concat(!String.IsNullOrEmpty(friendlyName) ? friendlyName : Guid.NewGuid().ToString("N"), ".xml"); - _logger.LogTrace("Saving element: {File}.", path); + using var memoryStream = new MemoryStream(); + element.Save(memoryStream, SaveOptions.DisableFormatting); + memoryStream.Seek(0, SeekOrigin.Begin); - return Run.WithRetriesAsync(async () => - { - using var memoryStream = new MemoryStream(); - element.Save(memoryStream, SaveOptions.DisableFormatting); - memoryStream.Seek(0, SeekOrigin.Begin); - - await _storage.SaveFileAsync(path, memoryStream).AnyContext(); - _logger.LogTrace("Saved element: {File}.", path); - }); - } + await _storage.SaveFileAsync(path, memoryStream).AnyContext(); + _logger.LogTrace("Saved element: {File}.", path); + }); } } diff --git a/src/Foundatio.Extensions.Hosting/Cronos/CalendarHelper.cs b/src/Foundatio.Extensions.Hosting/Cronos/CalendarHelper.cs index cde4f57b9..55e5ec523 100644 --- a/src/Foundatio.Extensions.Hosting/Cronos/CalendarHelper.cs +++ b/src/Foundatio.Extensions.Hosting/Cronos/CalendarHelper.cs @@ -5,171 +5,170 @@ using System; using System.Runtime.CompilerServices; -namespace Cronos +namespace Cronos; + +internal static class CalendarHelper { - internal static class CalendarHelper + private const int DaysPerWeekCount = 7; + + private const long TicksPerMillisecond = 10000; + private const long TicksPerSecond = TicksPerMillisecond * 1000; + private const long TicksPerMinute = TicksPerSecond * 60; + private const long TicksPerHour = TicksPerMinute * 60; + private const long TicksPerDay = TicksPerHour * 24; + + // Number of days in a non-leap year + private const int DaysPerYear = 365; + // Number of days in 4 years + private const int DaysPer4Years = DaysPerYear * 4 + 1; // 1461 + // Number of days in 100 years + private const int DaysPer100Years = DaysPer4Years * 25 - 1; // 36524 + // Number of days in 400 years + private const int DaysPer400Years = DaysPer100Years * 4 + 1; // 146097 + + private static readonly int[] DaysToMonth365 = + { + 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365 + }; + + private static readonly int[] DaysToMonth366 = { - private const int DaysPerWeekCount = 7; - - private const long TicksPerMillisecond = 10000; - private const long TicksPerSecond = TicksPerMillisecond * 1000; - private const long TicksPerMinute = TicksPerSecond * 60; - private const long TicksPerHour = TicksPerMinute * 60; - private const long TicksPerDay = TicksPerHour * 24; - - // Number of days in a non-leap year - private const int DaysPerYear = 365; - // Number of days in 4 years - private const int DaysPer4Years = DaysPerYear * 4 + 1; // 1461 - // Number of days in 100 years - private const int DaysPer100Years = DaysPer4Years * 25 - 1; // 36524 - // Number of days in 400 years - private const int DaysPer400Years = DaysPer100Years * 4 + 1; // 146097 - - private static readonly int[] DaysToMonth365 = - { - 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365 - }; - - private static readonly int[] DaysToMonth366 = - { - 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366 - }; - - private static readonly int[] DaysInMonth = - { - -1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 - }; + 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366 + }; + + private static readonly int[] DaysInMonth = + { + -1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 + }; #if !NET40 - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif - public static bool IsGreaterThan(int year1, int month1, int day1, int year2, int month2, int day2) - { - if (year1 != year2) return year1 > year2; - if (month1 != month2) return month1 > month2; - if (day2 != day1) return day1 > day2; - return false; - } + public static bool IsGreaterThan(int year1, int month1, int day1, int year2, int month2, int day2) + { + if (year1 != year2) return year1 > year2; + if (month1 != month2) return month1 > month2; + if (day2 != day1) return day1 > day2; + return false; + } #if !NET40 - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif - public static long DateTimeToTicks(int year, int month, int day, int hour, int minute, int second) - { - int[] days = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) ? DaysToMonth366 : DaysToMonth365; - int y = year - 1; - int n = y * 365 + y / 4 - y / 100 + y / 400 + days[month - 1] + day - 1; - return n * TicksPerDay + (hour * 3600L + minute * 60L + second) * TicksPerSecond; - } - - // Returns a given date part of this DateTime. This method is used - // to compute the year, day-of-year, month, or day part. + public static long DateTimeToTicks(int year, int month, int day, int hour, int minute, int second) + { + int[] days = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) ? DaysToMonth366 : DaysToMonth365; + int y = year - 1; + int n = y * 365 + y / 4 - y / 100 + y / 400 + days[month - 1] + day - 1; + return n * TicksPerDay + (hour * 3600L + minute * 60L + second) * TicksPerSecond; + } + + // Returns a given date part of this DateTime. This method is used + // to compute the year, day-of-year, month, or day part. #if !NET40 - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif - public static void FillDateTimeParts(long ticks, out int second, out int minute, out int hour, - out int day, out int month, out int year) - { - second = (int)(ticks / TicksPerSecond % 60); - if (ticks % TicksPerSecond != 0) second++; - minute = (int)(ticks / TicksPerMinute % 60); - hour = (int)(ticks / TicksPerHour % 24); - - // n = number of days since 1/1/0001 - int n = (int)(ticks / TicksPerDay); - // y400 = number of whole 400-year periods since 1/1/0001 - int y400 = n / DaysPer400Years; - // n = day number within 400-year period - n -= y400 * DaysPer400Years; - // y100 = number of whole 100-year periods within 400-year period - int y100 = n / DaysPer100Years; - // Last 100-year period has an extra day, so decrement result if 4 - if (y100 == 4) y100 = 3; - // n = day number within 100-year period - n -= y100 * DaysPer100Years; - // y4 = number of whole 4-year periods within 100-year period - int y4 = n / DaysPer4Years; - // n = day number within 4-year period - n -= y4 * DaysPer4Years; - // y1 = number of whole years within 4-year period - int y1 = n / DaysPerYear; - // Last year has an extra day, so decrement result if 4 - if (y1 == 4) y1 = 3; - // If year was requested, compute and return it - year = y400 * 400 + y100 * 100 + y4 * 4 + y1 + 1; - // n = day number within year - n -= y1 * DaysPerYear; - // Leap year calculation looks different from IsLeapYear since y1, y4, - // and y100 are relative to year 1, not year 0 - bool leapYear = y1 == 3 && (y4 != 24 || y100 == 3); - int[] days = leapYear ? DaysToMonth366 : DaysToMonth365; - // All months have less than 32 days, so n >> 5 is a good conservative - // estimate for the month - month = (n >> 5) + 1; - // m = 1-based month number - - // day = 1-based day-of-month - while (n >= days[month]) month++; - day = n - days[month - 1] + 1; - } + public static void FillDateTimeParts(long ticks, out int second, out int minute, out int hour, + out int day, out int month, out int year) + { + second = (int)(ticks / TicksPerSecond % 60); + if (ticks % TicksPerSecond != 0) second++; + minute = (int)(ticks / TicksPerMinute % 60); + hour = (int)(ticks / TicksPerHour % 24); + + // n = number of days since 1/1/0001 + int n = (int)(ticks / TicksPerDay); + // y400 = number of whole 400-year periods since 1/1/0001 + int y400 = n / DaysPer400Years; + // n = day number within 400-year period + n -= y400 * DaysPer400Years; + // y100 = number of whole 100-year periods within 400-year period + int y100 = n / DaysPer100Years; + // Last 100-year period has an extra day, so decrement result if 4 + if (y100 == 4) y100 = 3; + // n = day number within 100-year period + n -= y100 * DaysPer100Years; + // y4 = number of whole 4-year periods within 100-year period + int y4 = n / DaysPer4Years; + // n = day number within 4-year period + n -= y4 * DaysPer4Years; + // y1 = number of whole years within 4-year period + int y1 = n / DaysPerYear; + // Last year has an extra day, so decrement result if 4 + if (y1 == 4) y1 = 3; + // If year was requested, compute and return it + year = y400 * 400 + y100 * 100 + y4 * 4 + y1 + 1; + // n = day number within year + n -= y1 * DaysPerYear; + // Leap year calculation looks different from IsLeapYear since y1, y4, + // and y100 are relative to year 1, not year 0 + bool leapYear = y1 == 3 && (y4 != 24 || y100 == 3); + int[] days = leapYear ? DaysToMonth366 : DaysToMonth365; + // All months have less than 32 days, so n >> 5 is a good conservative + // estimate for the month + month = (n >> 5) + 1; + // m = 1-based month number + + // day = 1-based day-of-month + while (n >= days[month]) month++; + day = n - days[month - 1] + 1; + } #if !NET40 - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif - public static DayOfWeek GetDayOfWeek(int year, int month, int day) - { - var isLeapYear = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0); - int[] days = isLeapYear ? DaysToMonth366 : DaysToMonth365; - int y = year - 1; - int n = y * 365 + y / 4 - y / 100 + y / 400 + days[month - 1] + day - 1; - var ticks = n * TicksPerDay; + public static DayOfWeek GetDayOfWeek(int year, int month, int day) + { + var isLeapYear = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0); + int[] days = isLeapYear ? DaysToMonth366 : DaysToMonth365; + int y = year - 1; + int n = y * 365 + y / 4 - y / 100 + y / 400 + days[month - 1] + day - 1; + var ticks = n * TicksPerDay; - return ((DayOfWeek)((int)(ticks / TicksPerDay + 1) % 7)); - } + return ((DayOfWeek)((int)(ticks / TicksPerDay + 1) % 7)); + } #if !NET40 - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif - public static int GetDaysInMonth(int year, int month) - { - if (month != 2 || year % 4 != 0) return DaysInMonth[month]; + public static int GetDaysInMonth(int year, int month) + { + if (month != 2 || year % 4 != 0) return DaysInMonth[month]; - return year % 100 != 0 || year % 400 == 0 ? 29 : 28; - } + return year % 100 != 0 || year % 400 == 0 ? 29 : 28; + } #if !NET40 - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif - public static int MoveToNearestWeekDay(int year, int month, int day) - { - var dayOfWeek = GetDayOfWeek(year, month, day); + public static int MoveToNearestWeekDay(int year, int month, int day) + { + var dayOfWeek = GetDayOfWeek(year, month, day); - if (dayOfWeek != DayOfWeek.Saturday && dayOfWeek != DayOfWeek.Sunday) return day; + if (dayOfWeek != DayOfWeek.Saturday && dayOfWeek != DayOfWeek.Sunday) return day; - return dayOfWeek == DayOfWeek.Sunday - ? day == GetDaysInMonth(year, month) - ? day - 2 - : day + 1 - : day == CronField.DaysOfMonth.First - ? day + 2 - : day - 1; - } + return dayOfWeek == DayOfWeek.Sunday + ? day == GetDaysInMonth(year, month) + ? day - 2 + : day + 1 + : day == CronField.DaysOfMonth.First + ? day + 2 + : day - 1; + } #if !NET40 - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif - public static bool IsNthDayOfWeek(int day, int n) - { - return day - DaysPerWeekCount * n < CronField.DaysOfMonth.First && - day - DaysPerWeekCount * (n - 1) >= CronField.DaysOfMonth.First; - } + public static bool IsNthDayOfWeek(int day, int n) + { + return day - DaysPerWeekCount * n < CronField.DaysOfMonth.First && + day - DaysPerWeekCount * (n - 1) >= CronField.DaysOfMonth.First; + } #if !NET40 - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif - public static bool IsLastDayOfWeek(int year, int month, int day) - { - return day + DaysPerWeekCount > GetDaysInMonth(year, month); - } + public static bool IsLastDayOfWeek(int year, int month, int day) + { + return day + DaysPerWeekCount > GetDaysInMonth(year, month); } } diff --git a/src/Foundatio.Extensions.Hosting/Cronos/CronExpression.cs b/src/Foundatio.Extensions.Hosting/Cronos/CronExpression.cs index 5d21988ff..27f131a0d 100644 --- a/src/Foundatio.Extensions.Hosting/Cronos/CronExpression.cs +++ b/src/Foundatio.Extensions.Hosting/Cronos/CronExpression.cs @@ -1,17 +1,17 @@ // The MIT License(MIT) -// +// // Copyright (c) 2017 Sergey Odinokov -// +// // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: -// +// // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. -// +// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,1073 +26,1072 @@ using System.Runtime.CompilerServices; using System.Text; -namespace Cronos +namespace Cronos; + +/// +/// Provides a parser and scheduler for cron expressions. +/// +public sealed class CronExpression : IEquatable { - /// - /// Provides a parser and scheduler for cron expressions. - /// - public sealed class CronExpression : IEquatable - { - private const long NotFound = 0; + private const long NotFound = 0; - private const int MinNthDayOfWeek = 1; - private const int MaxNthDayOfWeek = 5; - private const int SundayBits = 0b1000_0001; + private const int MinNthDayOfWeek = 1; + private const int MaxNthDayOfWeek = 5; + private const int SundayBits = 0b1000_0001; - private const int MaxYear = 2099; + private const int MaxYear = 2099; - private static readonly TimeZoneInfo UtcTimeZone = TimeZoneInfo.Utc; + private static readonly TimeZoneInfo UtcTimeZone = TimeZoneInfo.Utc; - private static readonly CronExpression Yearly = Parse("0 0 1 1 *"); - private static readonly CronExpression Weekly = Parse("0 0 * * 0"); - private static readonly CronExpression Monthly = Parse("0 0 1 * *"); - private static readonly CronExpression Daily = Parse("0 0 * * *"); - private static readonly CronExpression Hourly = Parse("0 * * * *"); - private static readonly CronExpression Minutely = Parse("* * * * *"); - private static readonly CronExpression Secondly = Parse("* * * * * *", CronFormat.IncludeSeconds); + private static readonly CronExpression Yearly = Parse("0 0 1 1 *"); + private static readonly CronExpression Weekly = Parse("0 0 * * 0"); + private static readonly CronExpression Monthly = Parse("0 0 1 * *"); + private static readonly CronExpression Daily = Parse("0 0 * * *"); + private static readonly CronExpression Hourly = Parse("0 * * * *"); + private static readonly CronExpression Minutely = Parse("* * * * *"); + private static readonly CronExpression Secondly = Parse("* * * * * *", CronFormat.IncludeSeconds); - private static readonly int[] DeBruijnPositions = - { - 0, 1, 2, 53, 3, 7, 54, 27, - 4, 38, 41, 8, 34, 55, 48, 28, - 62, 5, 39, 46, 44, 42, 22, 9, - 24, 35, 59, 56, 49, 18, 29, 11, - 63, 52, 6, 26, 37, 40, 33, 47, - 61, 45, 43, 21, 23, 58, 17, 10, - 51, 25, 36, 32, 60, 20, 57, 16, - 50, 31, 19, 15, 30, 14, 13, 12 - }; - - private long _second; // 60 bits -> from 0 bit to 59 bit - private long _minute; // 60 bits -> from 0 bit to 59 bit - private int _hour; // 24 bits -> from 0 bit to 23 bit - private int _dayOfMonth; // 31 bits -> from 1 bit to 31 bit - private short _month; // 12 bits -> from 1 bit to 12 bit - private byte _dayOfWeek; // 8 bits -> from 0 bit to 7 bit - - private byte _nthDayOfWeek; - private byte _lastMonthOffset; - - private CronExpressionFlag _flags; - - private CronExpression() - { - } + private static readonly int[] DeBruijnPositions = + { + 0, 1, 2, 53, 3, 7, 54, 27, + 4, 38, 41, 8, 34, 55, 48, 28, + 62, 5, 39, 46, 44, 42, 22, 9, + 24, 35, 59, 56, 49, 18, 29, 11, + 63, 52, 6, 26, 37, 40, 33, 47, + 61, 45, 43, 21, 23, 58, 17, 10, + 51, 25, 36, 32, 60, 20, 57, 16, + 50, 31, 19, 15, 30, 14, 13, 12 + }; + + private long _second; // 60 bits -> from 0 bit to 59 bit + private long _minute; // 60 bits -> from 0 bit to 59 bit + private int _hour; // 24 bits -> from 0 bit to 23 bit + private int _dayOfMonth; // 31 bits -> from 1 bit to 31 bit + private short _month; // 12 bits -> from 1 bit to 12 bit + private byte _dayOfWeek; // 8 bits -> from 0 bit to 7 bit + + private byte _nthDayOfWeek; + private byte _lastMonthOffset; + + private CronExpressionFlag _flags; + + private CronExpression() + { + } - /// - /// Constructs a new based on the specified - /// cron expression. It's supported expressions consisting of 5 fields: - /// minute, hour, day of month, month, day of week. - /// If you want to parse non-standard cron expressions use with specified CronFields argument. - /// See more: https://github.com/HangfireIO/Cronos - /// - public static CronExpression Parse(string expression) - { - return Parse(expression, CronFormat.Standard); - } + /// + /// Constructs a new based on the specified + /// cron expression. It's supported expressions consisting of 5 fields: + /// minute, hour, day of month, month, day of week. + /// If you want to parse non-standard cron expressions use with specified CronFields argument. + /// See more: https://github.com/HangfireIO/Cronos + /// + public static CronExpression Parse(string expression) + { + return Parse(expression, CronFormat.Standard); + } - /// - /// Constructs a new based on the specified - /// cron expression. It's supported expressions consisting of 5 or 6 fields: - /// second (optional), minute, hour, day of month, month, day of week. - /// See more: https://github.com/HangfireIO/Cronos - /// + /// + /// Constructs a new based on the specified + /// cron expression. It's supported expressions consisting of 5 or 6 fields: + /// second (optional), minute, hour, day of month, month, day of week. + /// See more: https://github.com/HangfireIO/Cronos + /// #if !NET40 - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif - public static unsafe CronExpression Parse(string expression, CronFormat format) + public static unsafe CronExpression Parse(string expression, CronFormat format) + { + if (string.IsNullOrEmpty(expression)) throw new ArgumentNullException(nameof(expression)); + + fixed (char* value = expression) { - if (string.IsNullOrEmpty(expression)) throw new ArgumentNullException(nameof(expression)); + var pointer = value; - fixed (char* value = expression) - { - var pointer = value; + SkipWhiteSpaces(ref pointer); + + CronExpression cronExpression; + if (Accept(ref pointer, '@')) + { + cronExpression = ParseMacro(ref pointer); SkipWhiteSpaces(ref pointer); - CronExpression cronExpression; + if (cronExpression == null || !IsEndOfString(*pointer)) ThrowFormatException("Macro: Unexpected character '{0}' on position {1}.", *pointer, pointer - value); - if (Accept(ref pointer, '@')) - { - cronExpression = ParseMacro(ref pointer); - SkipWhiteSpaces(ref pointer); + return cronExpression; + } - if (cronExpression == null || !IsEndOfString(*pointer)) ThrowFormatException("Macro: Unexpected character '{0}' on position {1}.", *pointer, pointer - value); + cronExpression = new CronExpression(); - return cronExpression; - } + if (format == CronFormat.IncludeSeconds) + { + cronExpression._second = ParseField(CronField.Seconds, ref pointer, ref cronExpression._flags); + ParseWhiteSpace(CronField.Seconds, ref pointer); + } + else + { + SetBit(ref cronExpression._second, CronField.Seconds.First); + } - cronExpression = new CronExpression(); + cronExpression._minute = ParseField(CronField.Minutes, ref pointer, ref cronExpression._flags); + ParseWhiteSpace(CronField.Minutes, ref pointer); - if (format == CronFormat.IncludeSeconds) - { - cronExpression._second = ParseField(CronField.Seconds, ref pointer, ref cronExpression._flags); - ParseWhiteSpace(CronField.Seconds, ref pointer); - } - else - { - SetBit(ref cronExpression._second, CronField.Seconds.First); - } + cronExpression._hour = (int)ParseField(CronField.Hours, ref pointer, ref cronExpression._flags); + ParseWhiteSpace(CronField.Hours, ref pointer); - cronExpression._minute = ParseField(CronField.Minutes, ref pointer, ref cronExpression._flags); - ParseWhiteSpace(CronField.Minutes, ref pointer); + cronExpression._dayOfMonth = (int)ParseDayOfMonth(ref pointer, ref cronExpression._flags, ref cronExpression._lastMonthOffset); + ParseWhiteSpace(CronField.DaysOfMonth, ref pointer); - cronExpression._hour = (int)ParseField(CronField.Hours, ref pointer, ref cronExpression._flags); - ParseWhiteSpace(CronField.Hours, ref pointer); + cronExpression._month = (short)ParseField(CronField.Months, ref pointer, ref cronExpression._flags); + ParseWhiteSpace(CronField.Months, ref pointer); - cronExpression._dayOfMonth = (int)ParseDayOfMonth(ref pointer, ref cronExpression._flags, ref cronExpression._lastMonthOffset); - ParseWhiteSpace(CronField.DaysOfMonth, ref pointer); + cronExpression._dayOfWeek = (byte)ParseDayOfWeek(ref pointer, ref cronExpression._flags, ref cronExpression._nthDayOfWeek); + ParseEndOfString(ref pointer); - cronExpression._month = (short)ParseField(CronField.Months, ref pointer, ref cronExpression._flags); - ParseWhiteSpace(CronField.Months, ref pointer); + // Make sundays equivalent. + if ((cronExpression._dayOfWeek & SundayBits) != 0) + { + cronExpression._dayOfWeek |= SundayBits; + } - cronExpression._dayOfWeek = (byte)ParseDayOfWeek(ref pointer, ref cronExpression._flags, ref cronExpression._nthDayOfWeek); - ParseEndOfString(ref pointer); + return cronExpression; + } + } - // Make sundays equivalent. - if ((cronExpression._dayOfWeek & SundayBits) != 0) - { - cronExpression._dayOfWeek |= SundayBits; - } + /// + /// Calculates next occurrence starting with (optionally ) in UTC time zone. + /// + public DateTime? GetNextOccurrence(DateTime fromUtc, bool inclusive = false) + { + if (fromUtc.Kind != DateTimeKind.Utc) ThrowWrongDateTimeKindException(nameof(fromUtc)); - return cronExpression; - } - } + var found = FindOccurence(fromUtc.Ticks, inclusive); + if (found == NotFound) return null; - /// - /// Calculates next occurrence starting with (optionally ) in UTC time zone. - /// - public DateTime? GetNextOccurrence(DateTime fromUtc, bool inclusive = false) + return new DateTime(found, DateTimeKind.Utc); + } + + /// + /// Returns the list of next occurrences within the given date/time range, + /// including and excluding + /// by default, and UTC time zone. When none of the occurrences found, an + /// empty list is returned. + /// + public IEnumerable GetOccurrences( + DateTime fromUtc, + DateTime toUtc, + bool fromInclusive = true, + bool toInclusive = false) + { + if (fromUtc > toUtc) ThrowFromShouldBeLessThanToException(nameof(fromUtc), nameof(toUtc)); + + for (var occurrence = GetNextOccurrence(fromUtc, fromInclusive); + occurrence < toUtc || occurrence == toUtc && toInclusive; + // ReSharper disable once RedundantArgumentDefaultValue + // ReSharper disable once ArgumentsStyleLiteral + occurrence = GetNextOccurrence(occurrence.Value, inclusive: false)) { - if (fromUtc.Kind != DateTimeKind.Utc) ThrowWrongDateTimeKindException(nameof(fromUtc)); + yield return occurrence.Value; + } + } + + /// + /// Calculates next occurrence starting with (optionally ) in given + /// + public DateTime? GetNextOccurrence(DateTime fromUtc, TimeZoneInfo zone, bool inclusive = false) + { + if (fromUtc.Kind != DateTimeKind.Utc) ThrowWrongDateTimeKindException(nameof(fromUtc)); + if (ReferenceEquals(zone, UtcTimeZone)) + { var found = FindOccurence(fromUtc.Ticks, inclusive); if (found == NotFound) return null; return new DateTime(found, DateTimeKind.Utc); } - /// - /// Returns the list of next occurrences within the given date/time range, - /// including and excluding - /// by default, and UTC time zone. When none of the occurrences found, an - /// empty list is returned. - /// - public IEnumerable GetOccurrences( - DateTime fromUtc, - DateTime toUtc, - bool fromInclusive = true, - bool toInclusive = false) - { - if (fromUtc > toUtc) ThrowFromShouldBeLessThanToException(nameof(fromUtc), nameof(toUtc)); + var zonedStart = TimeZoneInfo.ConvertTime(fromUtc, zone); + var zonedStartOffset = new DateTimeOffset(zonedStart, zonedStart - fromUtc); + var occurrence = GetOccurenceByZonedTimes(zonedStartOffset, zone, inclusive); + return occurrence?.UtcDateTime; + } - for (var occurrence = GetNextOccurrence(fromUtc, fromInclusive); - occurrence < toUtc || occurrence == toUtc && toInclusive; - // ReSharper disable once RedundantArgumentDefaultValue - // ReSharper disable once ArgumentsStyleLiteral - occurrence = GetNextOccurrence(occurrence.Value, inclusive: false)) - { - yield return occurrence.Value; - } - } + /// + /// Returns the list of next occurrences within the given date/time range, including + /// and excluding by default, and + /// specified time zone. When none of the occurrences found, an empty list is returned. + /// + public IEnumerable GetOccurrences( + DateTime fromUtc, + DateTime toUtc, + TimeZoneInfo zone, + bool fromInclusive = true, + bool toInclusive = false) + { + if (fromUtc > toUtc) ThrowFromShouldBeLessThanToException(nameof(fromUtc), nameof(toUtc)); - /// - /// Calculates next occurrence starting with (optionally ) in given - /// - public DateTime? GetNextOccurrence(DateTime fromUtc, TimeZoneInfo zone, bool inclusive = false) + for (var occurrence = GetNextOccurrence(fromUtc, zone, fromInclusive); + occurrence < toUtc || occurrence == toUtc && toInclusive; + // ReSharper disable once RedundantArgumentDefaultValue + // ReSharper disable once ArgumentsStyleLiteral + occurrence = GetNextOccurrence(occurrence.Value, zone, inclusive: false)) { - if (fromUtc.Kind != DateTimeKind.Utc) ThrowWrongDateTimeKindException(nameof(fromUtc)); - - if (ReferenceEquals(zone, UtcTimeZone)) - { - var found = FindOccurence(fromUtc.Ticks, inclusive); - if (found == NotFound) return null; - - return new DateTime(found, DateTimeKind.Utc); - } - - var zonedStart = TimeZoneInfo.ConvertTime(fromUtc, zone); - var zonedStartOffset = new DateTimeOffset(zonedStart, zonedStart - fromUtc); - var occurrence = GetOccurenceByZonedTimes(zonedStartOffset, zone, inclusive); - return occurrence?.UtcDateTime; + yield return occurrence.Value; } + } - /// - /// Returns the list of next occurrences within the given date/time range, including - /// and excluding by default, and - /// specified time zone. When none of the occurrences found, an empty list is returned. - /// - public IEnumerable GetOccurrences( - DateTime fromUtc, - DateTime toUtc, - TimeZoneInfo zone, - bool fromInclusive = true, - bool toInclusive = false) + /// + /// Calculates next occurrence starting with (optionally ) in given + /// + public DateTimeOffset? GetNextOccurrence(DateTimeOffset from, TimeZoneInfo zone, bool inclusive = false) + { + if (ReferenceEquals(zone, UtcTimeZone)) { - if (fromUtc > toUtc) ThrowFromShouldBeLessThanToException(nameof(fromUtc), nameof(toUtc)); + var found = FindOccurence(from.UtcTicks, inclusive); + if (found == NotFound) return null; - for (var occurrence = GetNextOccurrence(fromUtc, zone, fromInclusive); - occurrence < toUtc || occurrence == toUtc && toInclusive; - // ReSharper disable once RedundantArgumentDefaultValue - // ReSharper disable once ArgumentsStyleLiteral - occurrence = GetNextOccurrence(occurrence.Value, zone, inclusive: false)) - { - yield return occurrence.Value; - } + return new DateTimeOffset(found, TimeSpan.Zero); } - /// - /// Calculates next occurrence starting with (optionally ) in given - /// - public DateTimeOffset? GetNextOccurrence(DateTimeOffset from, TimeZoneInfo zone, bool inclusive = false) - { - if (ReferenceEquals(zone, UtcTimeZone)) - { - var found = FindOccurence(from.UtcTicks, inclusive); - if (found == NotFound) return null; + var zonedStart = TimeZoneInfo.ConvertTime(from, zone); + return GetOccurenceByZonedTimes(zonedStart, zone, inclusive); + } - return new DateTimeOffset(found, TimeSpan.Zero); - } + /// + /// Returns the list of occurrences within the given date/time offset range, + /// including and excluding by + /// default. When none of the occurrences found, an empty list is returned. + /// + public IEnumerable GetOccurrences( + DateTimeOffset from, + DateTimeOffset to, + TimeZoneInfo zone, + bool fromInclusive = true, + bool toInclusive = false) + { + if (from > to) ThrowFromShouldBeLessThanToException(nameof(from), nameof(to)); - var zonedStart = TimeZoneInfo.ConvertTime(from, zone); - return GetOccurenceByZonedTimes(zonedStart, zone, inclusive); + for (var occurrence = GetNextOccurrence(from, zone, fromInclusive); + occurrence < to || occurrence == to && toInclusive; + // ReSharper disable once RedundantArgumentDefaultValue + // ReSharper disable once ArgumentsStyleLiteral + occurrence = GetNextOccurrence(occurrence.Value, zone, inclusive: false)) + { + yield return occurrence.Value; } + } - /// - /// Returns the list of occurrences within the given date/time offset range, - /// including and excluding by - /// default. When none of the occurrences found, an empty list is returned. - /// - public IEnumerable GetOccurrences( - DateTimeOffset from, - DateTimeOffset to, - TimeZoneInfo zone, - bool fromInclusive = true, - bool toInclusive = false) - { - if (from > to) ThrowFromShouldBeLessThanToException(nameof(from), nameof(to)); + /// + public override string ToString() + { + var expressionBuilder = new StringBuilder(); - for (var occurrence = GetNextOccurrence(from, zone, fromInclusive); - occurrence < to || occurrence == to && toInclusive; - // ReSharper disable once RedundantArgumentDefaultValue - // ReSharper disable once ArgumentsStyleLiteral - occurrence = GetNextOccurrence(occurrence.Value, zone, inclusive: false)) - { - yield return occurrence.Value; - } - } + AppendFieldValue(expressionBuilder, CronField.Seconds, _second).Append(' '); + AppendFieldValue(expressionBuilder, CronField.Minutes, _minute).Append(' '); + AppendFieldValue(expressionBuilder, CronField.Hours, _hour).Append(' '); + AppendDayOfMonth(expressionBuilder, _dayOfMonth).Append(' '); + AppendFieldValue(expressionBuilder, CronField.Months, _month).Append(' '); + AppendDayOfWeek(expressionBuilder, _dayOfWeek); - /// - public override string ToString() - { - var expressionBuilder = new StringBuilder(); + return expressionBuilder.ToString(); + } - AppendFieldValue(expressionBuilder, CronField.Seconds, _second).Append(' '); - AppendFieldValue(expressionBuilder, CronField.Minutes, _minute).Append(' '); - AppendFieldValue(expressionBuilder, CronField.Hours, _hour).Append(' '); - AppendDayOfMonth(expressionBuilder, _dayOfMonth).Append(' '); - AppendFieldValue(expressionBuilder, CronField.Months, _month).Append(' '); - AppendDayOfWeek(expressionBuilder, _dayOfWeek); + /// + /// Determines whether the specified is equal to the current . + /// + /// The to compare with the current . + /// + /// true if the specified is equal to the current ; otherwise, false. + /// + public bool Equals(CronExpression other) + { + if (other == null) return false; + + return _second == other._second && + _minute == other._minute && + _hour == other._hour && + _dayOfMonth == other._dayOfMonth && + _month == other._month && + _dayOfWeek == other._dayOfWeek && + _nthDayOfWeek == other._nthDayOfWeek && + _lastMonthOffset == other._lastMonthOffset && + _flags == other._flags; + } - return expressionBuilder.ToString(); - } + /// + /// Determines whether the specified is equal to this instance. + /// + /// The to compare with this instance. + /// + /// true if the specified is equal to this instance; + /// otherwise, false. + /// + public override bool Equals(object obj) => Equals(obj as CronExpression); - /// - /// Determines whether the specified is equal to the current . - /// - /// The to compare with the current . - /// - /// true if the specified is equal to the current ; otherwise, false. - /// - public bool Equals(CronExpression other) + /// + /// Returns a hash code for this instance. + /// + /// + /// A hash code for this instance, suitable for use in hashing algorithms and data + /// structures like a hash table. + /// + [SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")] + public override int GetHashCode() + { + unchecked { - if (other == null) return false; - - return _second == other._second && - _minute == other._minute && - _hour == other._hour && - _dayOfMonth == other._dayOfMonth && - _month == other._month && - _dayOfWeek == other._dayOfWeek && - _nthDayOfWeek == other._nthDayOfWeek && - _lastMonthOffset == other._lastMonthOffset && - _flags == other._flags; + var hashCode = _second.GetHashCode(); + hashCode = (hashCode * 397) ^ _minute.GetHashCode(); + hashCode = (hashCode * 397) ^ _hour; + hashCode = (hashCode * 397) ^ _dayOfMonth; + hashCode = (hashCode * 397) ^ _month.GetHashCode(); + hashCode = (hashCode * 397) ^ _dayOfWeek.GetHashCode(); + hashCode = (hashCode * 397) ^ _nthDayOfWeek.GetHashCode(); + hashCode = (hashCode * 397) ^ _lastMonthOffset.GetHashCode(); + hashCode = (hashCode * 397) ^ (int)_flags; + + return hashCode; } + } - /// - /// Determines whether the specified is equal to this instance. - /// - /// The to compare with this instance. - /// - /// true if the specified is equal to this instance; - /// otherwise, false. - /// - public override bool Equals(object obj) => Equals(obj as CronExpression); - - /// - /// Returns a hash code for this instance. - /// - /// - /// A hash code for this instance, suitable for use in hashing algorithms and data - /// structures like a hash table. - /// - [SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")] - public override int GetHashCode() - { - unchecked - { - var hashCode = _second.GetHashCode(); - hashCode = (hashCode * 397) ^ _minute.GetHashCode(); - hashCode = (hashCode * 397) ^ _hour; - hashCode = (hashCode * 397) ^ _dayOfMonth; - hashCode = (hashCode * 397) ^ _month.GetHashCode(); - hashCode = (hashCode * 397) ^ _dayOfWeek.GetHashCode(); - hashCode = (hashCode * 397) ^ _nthDayOfWeek.GetHashCode(); - hashCode = (hashCode * 397) ^ _lastMonthOffset.GetHashCode(); - hashCode = (hashCode * 397) ^ (int)_flags; - - return hashCode; - } - } + /// + /// Implements the operator ==. + /// + public static bool operator ==(CronExpression left, CronExpression right) => Equals(left, right); - /// - /// Implements the operator ==. - /// - public static bool operator ==(CronExpression left, CronExpression right) => Equals(left, right); + /// + /// Implements the operator !=. + /// + public static bool operator !=(CronExpression left, CronExpression right) => !Equals(left, right); - /// - /// Implements the operator !=. - /// - public static bool operator !=(CronExpression left, CronExpression right) => !Equals(left, right); + private DateTimeOffset? GetOccurenceByZonedTimes(DateTimeOffset from, TimeZoneInfo zone, bool inclusive) + { + var fromLocal = from.DateTime; - private DateTimeOffset? GetOccurenceByZonedTimes(DateTimeOffset from, TimeZoneInfo zone, bool inclusive) + if (TimeZoneHelper.IsAmbiguousTime(zone, fromLocal)) { - var fromLocal = from.DateTime; + var currentOffset = from.Offset; + var standardOffset = zone.BaseUtcOffset; - if (TimeZoneHelper.IsAmbiguousTime(zone, fromLocal)) + if (standardOffset != currentOffset) { - var currentOffset = from.Offset; - var standardOffset = zone.BaseUtcOffset; + var daylightOffset = TimeZoneHelper.GetDaylightOffset(zone, fromLocal); + var daylightTimeLocalEnd = TimeZoneHelper.GetDaylightTimeEnd(zone, fromLocal, daylightOffset).DateTime; - if (standardOffset != currentOffset) - { - var daylightOffset = TimeZoneHelper.GetDaylightOffset(zone, fromLocal); - var daylightTimeLocalEnd = TimeZoneHelper.GetDaylightTimeEnd(zone, fromLocal, daylightOffset).DateTime; - - // Early period, try to find anything here. - var foundInDaylightOffset = FindOccurence(fromLocal.Ticks, daylightTimeLocalEnd.Ticks, inclusive); - if (foundInDaylightOffset != NotFound) return new DateTimeOffset(foundInDaylightOffset, daylightOffset); + // Early period, try to find anything here. + var foundInDaylightOffset = FindOccurence(fromLocal.Ticks, daylightTimeLocalEnd.Ticks, inclusive); + if (foundInDaylightOffset != NotFound) return new DateTimeOffset(foundInDaylightOffset, daylightOffset); - fromLocal = TimeZoneHelper.GetStandardTimeStart(zone, fromLocal, daylightOffset).DateTime; - inclusive = true; - } - - // Skip late ambiguous interval. - var ambiguousIntervalLocalEnd = TimeZoneHelper.GetAmbiguousIntervalEnd(zone, fromLocal).DateTime; - - if (HasFlag(CronExpressionFlag.Interval)) - { - var foundInStandardOffset = FindOccurence(fromLocal.Ticks, ambiguousIntervalLocalEnd.Ticks - 1, inclusive); - if (foundInStandardOffset != NotFound) return new DateTimeOffset(foundInStandardOffset, standardOffset); - } - - fromLocal = ambiguousIntervalLocalEnd; + fromLocal = TimeZoneHelper.GetStandardTimeStart(zone, fromLocal, daylightOffset).DateTime; inclusive = true; } - var occurrenceTicks = FindOccurence(fromLocal.Ticks, inclusive); - if (occurrenceTicks == NotFound) return null; - - var occurrence = new DateTime(occurrenceTicks); + // Skip late ambiguous interval. + var ambiguousIntervalLocalEnd = TimeZoneHelper.GetAmbiguousIntervalEnd(zone, fromLocal).DateTime; - if (zone.IsInvalidTime(occurrence)) + if (HasFlag(CronExpressionFlag.Interval)) { - var nextValidTime = TimeZoneHelper.GetDaylightTimeStart(zone, occurrence); - return nextValidTime; + var foundInStandardOffset = FindOccurence(fromLocal.Ticks, ambiguousIntervalLocalEnd.Ticks - 1, inclusive); + if (foundInStandardOffset != NotFound) return new DateTimeOffset(foundInStandardOffset, standardOffset); } - if (TimeZoneHelper.IsAmbiguousTime(zone, occurrence)) - { - var daylightOffset = TimeZoneHelper.GetDaylightOffset(zone, occurrence); - return new DateTimeOffset(occurrence, daylightOffset); - } - - return new DateTimeOffset(occurrence, zone.GetUtcOffset(occurrence)); + fromLocal = ambiguousIntervalLocalEnd; + inclusive = true; } - private long FindOccurence(long startTimeTicks, long endTimeTicks, bool startInclusive) - { - var found = FindOccurence(startTimeTicks, startInclusive); + var occurrenceTicks = FindOccurence(fromLocal.Ticks, inclusive); + if (occurrenceTicks == NotFound) return null; + + var occurrence = new DateTime(occurrenceTicks); - if (found == NotFound || found > endTimeTicks) return NotFound; - return found; + if (zone.IsInvalidTime(occurrence)) + { + var nextValidTime = TimeZoneHelper.GetDaylightTimeStart(zone, occurrence); + return nextValidTime; } - private long FindOccurence(long ticks, bool startInclusive) + if (TimeZoneHelper.IsAmbiguousTime(zone, occurrence)) { - if (!startInclusive) ticks++; + var daylightOffset = TimeZoneHelper.GetDaylightOffset(zone, occurrence); + return new DateTimeOffset(occurrence, daylightOffset); + } - CalendarHelper.FillDateTimeParts( - ticks, - out int startSecond, - out int startMinute, - out int startHour, - out int startDay, - out int startMonth, - out int startYear); + return new DateTimeOffset(occurrence, zone.GetUtcOffset(occurrence)); + } - var minMatchedDay = GetFirstSet(_dayOfMonth); + private long FindOccurence(long startTimeTicks, long endTimeTicks, bool startInclusive) + { + var found = FindOccurence(startTimeTicks, startInclusive); - var second = startSecond; - var minute = startMinute; - var hour = startHour; - var day = startDay; - var month = startMonth; - var year = startYear; + if (found == NotFound || found > endTimeTicks) return NotFound; + return found; + } - if (!GetBit(_second, second) && !Move(_second, ref second)) minute++; - if (!GetBit(_minute, minute) && !Move(_minute, ref minute)) hour++; - if (!GetBit(_hour, hour) && !Move(_hour, ref hour)) day++; + private long FindOccurence(long ticks, bool startInclusive) + { + if (!startInclusive) ticks++; - // If NearestWeekday flag is set it's possible forward shift. - if (HasFlag(CronExpressionFlag.NearestWeekday)) day = CronField.DaysOfMonth.First; + CalendarHelper.FillDateTimeParts( + ticks, + out int startSecond, + out int startMinute, + out int startHour, + out int startDay, + out int startMonth, + out int startYear); - if (!GetBit(_dayOfMonth, day) && !Move(_dayOfMonth, ref day)) goto RetryMonth; - if (!GetBit(_month, month)) goto RetryMonth; + var minMatchedDay = GetFirstSet(_dayOfMonth); - Retry: + var second = startSecond; + var minute = startMinute; + var hour = startHour; + var day = startDay; + var month = startMonth; + var year = startYear; - if (day > GetLastDayOfMonth(year, month)) goto RetryMonth; + if (!GetBit(_second, second) && !Move(_second, ref second)) minute++; + if (!GetBit(_minute, minute) && !Move(_minute, ref minute)) hour++; + if (!GetBit(_hour, hour) && !Move(_hour, ref hour)) day++; - if (HasFlag(CronExpressionFlag.DayOfMonthLast)) day = GetLastDayOfMonth(year, month); + // If NearestWeekday flag is set it's possible forward shift. + if (HasFlag(CronExpressionFlag.NearestWeekday)) day = CronField.DaysOfMonth.First; - var lastCheckedDay = day; + if (!GetBit(_dayOfMonth, day) && !Move(_dayOfMonth, ref day)) goto RetryMonth; + if (!GetBit(_month, month)) goto RetryMonth; - if (HasFlag(CronExpressionFlag.NearestWeekday)) day = CalendarHelper.MoveToNearestWeekDay(year, month, day); + Retry: - if (IsDayOfWeekMatch(year, month, day)) - { - if (CalendarHelper.IsGreaterThan(year, month, day, startYear, startMonth, startDay)) goto RolloverDay; - if (hour > startHour) goto RolloverHour; - if (minute > startMinute) goto RolloverMinute; - goto ReturnResult; + if (day > GetLastDayOfMonth(year, month)) goto RetryMonth; - RolloverDay: hour = GetFirstSet(_hour); - RolloverHour: minute = GetFirstSet(_minute); - RolloverMinute: second = GetFirstSet(_second); + if (HasFlag(CronExpressionFlag.DayOfMonthLast)) day = GetLastDayOfMonth(year, month); - ReturnResult: + var lastCheckedDay = day; - var found = CalendarHelper.DateTimeToTicks(year, month, day, hour, minute, second); - if (found >= ticks) return found; - } + if (HasFlag(CronExpressionFlag.NearestWeekday)) day = CalendarHelper.MoveToNearestWeekDay(year, month, day); - day = lastCheckedDay; - if (Move(_dayOfMonth, ref day)) goto Retry; + if (IsDayOfWeekMatch(year, month, day)) + { + if (CalendarHelper.IsGreaterThan(year, month, day, startYear, startMonth, startDay)) goto RolloverDay; + if (hour > startHour) goto RolloverHour; + if (minute > startMinute) goto RolloverMinute; + goto ReturnResult; - RetryMonth: + RolloverDay: hour = GetFirstSet(_hour); + RolloverHour: minute = GetFirstSet(_minute); + RolloverMinute: second = GetFirstSet(_second); - if (!Move(_month, ref month) && ++year >= MaxYear) return NotFound; - day = minMatchedDay; + ReturnResult: - goto Retry; + var found = CalendarHelper.DateTimeToTicks(year, month, day, hour, minute, second); + if (found >= ticks) return found; } - private static bool Move(long fieldBits, ref int fieldValue) - { - if (fieldBits >> ++fieldValue == 0) - { - fieldValue = GetFirstSet(fieldBits); - return false; - } + day = lastCheckedDay; + if (Move(_dayOfMonth, ref day)) goto Retry; - fieldValue += GetFirstSet(fieldBits >> fieldValue); - return true; - } + RetryMonth: + + if (!Move(_month, ref month) && ++year >= MaxYear) return NotFound; + day = minMatchedDay; - private int GetLastDayOfMonth(int year, int month) + goto Retry; + } + + private static bool Move(long fieldBits, ref int fieldValue) + { + if (fieldBits >> ++fieldValue == 0) { - return CalendarHelper.GetDaysInMonth(year, month) - _lastMonthOffset; + fieldValue = GetFirstSet(fieldBits); + return false; } - private bool IsDayOfWeekMatch(int year, int month, int day) + fieldValue += GetFirstSet(fieldBits >> fieldValue); + return true; + } + + private int GetLastDayOfMonth(int year, int month) + { + return CalendarHelper.GetDaysInMonth(year, month) - _lastMonthOffset; + } + + private bool IsDayOfWeekMatch(int year, int month, int day) + { + if (HasFlag(CronExpressionFlag.DayOfWeekLast) && !CalendarHelper.IsLastDayOfWeek(year, month, day) || + HasFlag(CronExpressionFlag.NthDayOfWeek) && !CalendarHelper.IsNthDayOfWeek(day, _nthDayOfWeek)) { - if (HasFlag(CronExpressionFlag.DayOfWeekLast) && !CalendarHelper.IsLastDayOfWeek(year, month, day) || - HasFlag(CronExpressionFlag.NthDayOfWeek) && !CalendarHelper.IsNthDayOfWeek(day, _nthDayOfWeek)) - { - return false; - } + return false; + } - if (_dayOfWeek == CronField.DaysOfWeek.AllBits) return true; + if (_dayOfWeek == CronField.DaysOfWeek.AllBits) return true; - var dayOfWeek = CalendarHelper.GetDayOfWeek(year, month, day); + var dayOfWeek = CalendarHelper.GetDayOfWeek(year, month, day); - return ((_dayOfWeek >> (int)dayOfWeek) & 1) != 0; - } + return ((_dayOfWeek >> (int)dayOfWeek) & 1) != 0; + } #if !NET40 - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif - private static int GetFirstSet(long value) - { - // TODO: Add description and source - ulong res = unchecked((ulong)(value & -value) * 0x022fdd63cc95386d) >> 58; - return DeBruijnPositions[res]; - } + private static int GetFirstSet(long value) + { + // TODO: Add description and source + ulong res = unchecked((ulong)(value & -value) * 0x022fdd63cc95386d) >> 58; + return DeBruijnPositions[res]; + } - private bool HasFlag(CronExpressionFlag value) - { - return (_flags & value) != 0; - } + private bool HasFlag(CronExpressionFlag value) + { + return (_flags & value) != 0; + } #if !NET40 - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif - private static unsafe void SkipWhiteSpaces(ref char* pointer) - { - while (IsWhiteSpace(*pointer)) { pointer++; } - } + private static unsafe void SkipWhiteSpaces(ref char* pointer) + { + while (IsWhiteSpace(*pointer)) { pointer++; } + } #if !NET40 - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif - private static unsafe void ParseWhiteSpace(CronField prevField, ref char* pointer) - { - if (!IsWhiteSpace(*pointer)) ThrowFormatException(prevField, "Unexpected character '{0}'.", *pointer); - SkipWhiteSpaces(ref pointer); - } + private static unsafe void ParseWhiteSpace(CronField prevField, ref char* pointer) + { + if (!IsWhiteSpace(*pointer)) ThrowFormatException(prevField, "Unexpected character '{0}'.", *pointer); + SkipWhiteSpaces(ref pointer); + } #if !NET40 - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif - private static unsafe void ParseEndOfString(ref char* pointer) - { - if (!IsWhiteSpace(*pointer) && !IsEndOfString(*pointer)) ThrowFormatException(CronField.DaysOfWeek, "Unexpected character '{0}'.", *pointer); + private static unsafe void ParseEndOfString(ref char* pointer) + { + if (!IsWhiteSpace(*pointer) && !IsEndOfString(*pointer)) ThrowFormatException(CronField.DaysOfWeek, "Unexpected character '{0}'.", *pointer); - SkipWhiteSpaces(ref pointer); - if (!IsEndOfString(*pointer)) ThrowFormatException("Unexpected character '{0}'.", *pointer); - } + SkipWhiteSpaces(ref pointer); + if (!IsEndOfString(*pointer)) ThrowFormatException("Unexpected character '{0}'.", *pointer); + } - private static unsafe CronExpression ParseMacro(ref char* pointer) + private static unsafe CronExpression ParseMacro(ref char* pointer) + { + switch (ToUpper(*pointer++)) { - switch (ToUpper(*pointer++)) - { - case 'A': - if (AcceptCharacter(ref pointer, 'N') && - AcceptCharacter(ref pointer, 'N') && - AcceptCharacter(ref pointer, 'U') && - AcceptCharacter(ref pointer, 'A') && - AcceptCharacter(ref pointer, 'L') && - AcceptCharacter(ref pointer, 'L') && - AcceptCharacter(ref pointer, 'Y')) - return Yearly; - return null; - case 'D': - if (AcceptCharacter(ref pointer, 'A') && + case 'A': + if (AcceptCharacter(ref pointer, 'N') && + AcceptCharacter(ref pointer, 'N') && + AcceptCharacter(ref pointer, 'U') && + AcceptCharacter(ref pointer, 'A') && + AcceptCharacter(ref pointer, 'L') && + AcceptCharacter(ref pointer, 'L') && + AcceptCharacter(ref pointer, 'Y')) + return Yearly; + return null; + case 'D': + if (AcceptCharacter(ref pointer, 'A') && + AcceptCharacter(ref pointer, 'I') && + AcceptCharacter(ref pointer, 'L') && + AcceptCharacter(ref pointer, 'Y')) + return Daily; + return null; + case 'E': + if (AcceptCharacter(ref pointer, 'V') && + AcceptCharacter(ref pointer, 'E') && + AcceptCharacter(ref pointer, 'R') && + AcceptCharacter(ref pointer, 'Y') && + Accept(ref pointer, '_')) + { + if (AcceptCharacter(ref pointer, 'M') && AcceptCharacter(ref pointer, 'I') && - AcceptCharacter(ref pointer, 'L') && - AcceptCharacter(ref pointer, 'Y')) - return Daily; - return null; - case 'E': - if (AcceptCharacter(ref pointer, 'V') && - AcceptCharacter(ref pointer, 'E') && - AcceptCharacter(ref pointer, 'R') && - AcceptCharacter(ref pointer, 'Y') && - Accept(ref pointer, '_')) - { - if (AcceptCharacter(ref pointer, 'M') && - AcceptCharacter(ref pointer, 'I') && - AcceptCharacter(ref pointer, 'N') && - AcceptCharacter(ref pointer, 'U') && - AcceptCharacter(ref pointer, 'T') && - AcceptCharacter(ref pointer, 'E')) - return Minutely; - - if (*(pointer - 1) != '_') return null; - - if (AcceptCharacter(ref pointer, 'S') && - AcceptCharacter(ref pointer, 'E') && - AcceptCharacter(ref pointer, 'C') && - AcceptCharacter(ref pointer, 'O') && - AcceptCharacter(ref pointer, 'N') && - AcceptCharacter(ref pointer, 'D')) - return Secondly; - } - - return null; - case 'H': - if (AcceptCharacter(ref pointer, 'O') && - AcceptCharacter(ref pointer, 'U') && - AcceptCharacter(ref pointer, 'R') && - AcceptCharacter(ref pointer, 'L') && - AcceptCharacter(ref pointer, 'Y')) - return Hourly; - return null; - case 'M': - if (AcceptCharacter(ref pointer, 'O') && AcceptCharacter(ref pointer, 'N') && + AcceptCharacter(ref pointer, 'U') && AcceptCharacter(ref pointer, 'T') && - AcceptCharacter(ref pointer, 'H') && - AcceptCharacter(ref pointer, 'L') && - AcceptCharacter(ref pointer, 'Y')) - return Monthly; + AcceptCharacter(ref pointer, 'E')) + return Minutely; - if (ToUpper(*(pointer - 1)) == 'M' && - AcceptCharacter(ref pointer, 'I') && - AcceptCharacter(ref pointer, 'D') && - AcceptCharacter(ref pointer, 'N') && - AcceptCharacter(ref pointer, 'I') && - AcceptCharacter(ref pointer, 'G') && - AcceptCharacter(ref pointer, 'H') && - AcceptCharacter(ref pointer, 'T')) - return Daily; - - return null; - case 'W': - if (AcceptCharacter(ref pointer, 'E') && + if (*(pointer - 1) != '_') return null; + + if (AcceptCharacter(ref pointer, 'S') && AcceptCharacter(ref pointer, 'E') && - AcceptCharacter(ref pointer, 'K') && - AcceptCharacter(ref pointer, 'L') && - AcceptCharacter(ref pointer, 'Y')) - return Weekly; - return null; - case 'Y': - if (AcceptCharacter(ref pointer, 'E') && - AcceptCharacter(ref pointer, 'A') && - AcceptCharacter(ref pointer, 'R') && - AcceptCharacter(ref pointer, 'L') && - AcceptCharacter(ref pointer, 'Y')) - return Yearly; - return null; - default: - pointer--; - return null; - } + AcceptCharacter(ref pointer, 'C') && + AcceptCharacter(ref pointer, 'O') && + AcceptCharacter(ref pointer, 'N') && + AcceptCharacter(ref pointer, 'D')) + return Secondly; + } + + return null; + case 'H': + if (AcceptCharacter(ref pointer, 'O') && + AcceptCharacter(ref pointer, 'U') && + AcceptCharacter(ref pointer, 'R') && + AcceptCharacter(ref pointer, 'L') && + AcceptCharacter(ref pointer, 'Y')) + return Hourly; + return null; + case 'M': + if (AcceptCharacter(ref pointer, 'O') && + AcceptCharacter(ref pointer, 'N') && + AcceptCharacter(ref pointer, 'T') && + AcceptCharacter(ref pointer, 'H') && + AcceptCharacter(ref pointer, 'L') && + AcceptCharacter(ref pointer, 'Y')) + return Monthly; + + if (ToUpper(*(pointer - 1)) == 'M' && + AcceptCharacter(ref pointer, 'I') && + AcceptCharacter(ref pointer, 'D') && + AcceptCharacter(ref pointer, 'N') && + AcceptCharacter(ref pointer, 'I') && + AcceptCharacter(ref pointer, 'G') && + AcceptCharacter(ref pointer, 'H') && + AcceptCharacter(ref pointer, 'T')) + return Daily; + + return null; + case 'W': + if (AcceptCharacter(ref pointer, 'E') && + AcceptCharacter(ref pointer, 'E') && + AcceptCharacter(ref pointer, 'K') && + AcceptCharacter(ref pointer, 'L') && + AcceptCharacter(ref pointer, 'Y')) + return Weekly; + return null; + case 'Y': + if (AcceptCharacter(ref pointer, 'E') && + AcceptCharacter(ref pointer, 'A') && + AcceptCharacter(ref pointer, 'R') && + AcceptCharacter(ref pointer, 'L') && + AcceptCharacter(ref pointer, 'Y')) + return Yearly; + return null; + default: + pointer--; + return null; } + } - private static unsafe long ParseField(CronField field, ref char* pointer, ref CronExpressionFlag flags) + private static unsafe long ParseField(CronField field, ref char* pointer, ref CronExpressionFlag flags) + { + if (Accept(ref pointer, '*') || Accept(ref pointer, '?')) { - if (Accept(ref pointer, '*') || Accept(ref pointer, '?')) - { - if (field.CanDefineInterval) flags |= CronExpressionFlag.Interval; - return ParseStar(field, ref pointer); - } + if (field.CanDefineInterval) flags |= CronExpressionFlag.Interval; + return ParseStar(field, ref pointer); + } - var num = ParseValue(field, ref pointer); + var num = ParseValue(field, ref pointer); - var bits = ParseRange(field, ref pointer, num, ref flags); - if (Accept(ref pointer, ',')) bits |= ParseList(field, ref pointer, ref flags); + var bits = ParseRange(field, ref pointer, num, ref flags); + if (Accept(ref pointer, ',')) bits |= ParseList(field, ref pointer, ref flags); - return bits; - } + return bits; + } - private static unsafe long ParseDayOfMonth(ref char* pointer, ref CronExpressionFlag flags, ref byte lastDayOffset) - { - var field = CronField.DaysOfMonth; + private static unsafe long ParseDayOfMonth(ref char* pointer, ref CronExpressionFlag flags, ref byte lastDayOffset) + { + var field = CronField.DaysOfMonth; - if (Accept(ref pointer, '*') || Accept(ref pointer, '?')) return ParseStar(field, ref pointer); + if (Accept(ref pointer, '*') || Accept(ref pointer, '?')) return ParseStar(field, ref pointer); - if (AcceptCharacter(ref pointer, 'L')) return ParseLastDayOfMonth(field, ref pointer, ref flags, ref lastDayOffset); + if (AcceptCharacter(ref pointer, 'L')) return ParseLastDayOfMonth(field, ref pointer, ref flags, ref lastDayOffset); - var dayOfMonth = ParseValue(field, ref pointer); + var dayOfMonth = ParseValue(field, ref pointer); - if (AcceptCharacter(ref pointer, 'W')) - { - flags |= CronExpressionFlag.NearestWeekday; - return GetBit(dayOfMonth); - } + if (AcceptCharacter(ref pointer, 'W')) + { + flags |= CronExpressionFlag.NearestWeekday; + return GetBit(dayOfMonth); + } - var bits = ParseRange(field, ref pointer, dayOfMonth, ref flags); - if (Accept(ref pointer, ',')) bits |= ParseList(field, ref pointer, ref flags); + var bits = ParseRange(field, ref pointer, dayOfMonth, ref flags); + if (Accept(ref pointer, ',')) bits |= ParseList(field, ref pointer, ref flags); - return bits; - } + return bits; + } #if !NET40 - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif - private static unsafe long ParseDayOfWeek(ref char* pointer, ref CronExpressionFlag flags, ref byte nthWeekDay) - { - var field = CronField.DaysOfWeek; - if (Accept(ref pointer, '*') || Accept(ref pointer, '?')) return ParseStar(field, ref pointer); + private static unsafe long ParseDayOfWeek(ref char* pointer, ref CronExpressionFlag flags, ref byte nthWeekDay) + { + var field = CronField.DaysOfWeek; + if (Accept(ref pointer, '*') || Accept(ref pointer, '?')) return ParseStar(field, ref pointer); - var dayOfWeek = ParseValue(field, ref pointer); + var dayOfWeek = ParseValue(field, ref pointer); - if (AcceptCharacter(ref pointer, 'L')) return ParseLastWeekDay(dayOfWeek, ref flags); - if (Accept(ref pointer, '#')) return ParseNthWeekDay(field, ref pointer, dayOfWeek, ref flags, out nthWeekDay); + if (AcceptCharacter(ref pointer, 'L')) return ParseLastWeekDay(dayOfWeek, ref flags); + if (Accept(ref pointer, '#')) return ParseNthWeekDay(field, ref pointer, dayOfWeek, ref flags, out nthWeekDay); - var bits = ParseRange(field, ref pointer, dayOfWeek, ref flags); - if (Accept(ref pointer, ',')) bits |= ParseList(field, ref pointer, ref flags); + var bits = ParseRange(field, ref pointer, dayOfWeek, ref flags); + if (Accept(ref pointer, ',')) bits |= ParseList(field, ref pointer, ref flags); - return bits; - } + return bits; + } #if !NET40 - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif - private static unsafe long ParseStar(CronField field, ref char* pointer) - { - return Accept(ref pointer, '/') - ? ParseStep(field, ref pointer, field.First, field.Last) - : field.AllBits; - } + private static unsafe long ParseStar(CronField field, ref char* pointer) + { + return Accept(ref pointer, '/') + ? ParseStep(field, ref pointer, field.First, field.Last) + : field.AllBits; + } #if !NET40 - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif - private static unsafe long ParseList(CronField field, ref char* pointer, ref CronExpressionFlag flags) - { - var num = ParseValue(field, ref pointer); - var bits = ParseRange(field, ref pointer, num, ref flags); + private static unsafe long ParseList(CronField field, ref char* pointer, ref CronExpressionFlag flags) + { + var num = ParseValue(field, ref pointer); + var bits = ParseRange(field, ref pointer, num, ref flags); - do - { - if (!Accept(ref pointer, ',')) return bits; + do + { + if (!Accept(ref pointer, ',')) return bits; - bits |= ParseList(field, ref pointer, ref flags); - } while (true); - } + bits |= ParseList(field, ref pointer, ref flags); + } while (true); + } - private static unsafe long ParseRange(CronField field, ref char* pointer, int low, ref CronExpressionFlag flags) + private static unsafe long ParseRange(CronField field, ref char* pointer, int low, ref CronExpressionFlag flags) + { + if (!Accept(ref pointer, '-')) { - if (!Accept(ref pointer, '-')) - { - if (!Accept(ref pointer, '/')) return GetBit(low); - - if (field.CanDefineInterval) flags |= CronExpressionFlag.Interval; - return ParseStep(field, ref pointer, low, field.Last); - } + if (!Accept(ref pointer, '/')) return GetBit(low); if (field.CanDefineInterval) flags |= CronExpressionFlag.Interval; - - var high = ParseValue(field, ref pointer); - if (Accept(ref pointer, '/')) return ParseStep(field, ref pointer, low, high); - return GetBits(field, low, high, 1); + return ParseStep(field, ref pointer, low, field.Last); } - private static unsafe long ParseStep(CronField field, ref char* pointer, int low, int high) - { - // Get the step size -- note: we don't pass the - // names here, because the number is not an - // element id, it's a step size. 'low' is - // sent as a 0 since there is no offset either. - var step = ParseNumber(field, ref pointer, 1, field.Last); - return GetBits(field, low, high, step); - } + if (field.CanDefineInterval) flags |= CronExpressionFlag.Interval; + + var high = ParseValue(field, ref pointer); + if (Accept(ref pointer, '/')) return ParseStep(field, ref pointer, low, high); + return GetBits(field, low, high, 1); + } + + private static unsafe long ParseStep(CronField field, ref char* pointer, int low, int high) + { + // Get the step size -- note: we don't pass the + // names here, because the number is not an + // element id, it's a step size. 'low' is + // sent as a 0 since there is no offset either. + var step = ParseNumber(field, ref pointer, 1, field.Last); + return GetBits(field, low, high, step); + } #if !NET40 - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif - private static unsafe long ParseLastDayOfMonth(CronField field, ref char* pointer, ref CronExpressionFlag flags, ref byte lastMonthOffset) - { - flags |= CronExpressionFlag.DayOfMonthLast; + private static unsafe long ParseLastDayOfMonth(CronField field, ref char* pointer, ref CronExpressionFlag flags, ref byte lastMonthOffset) + { + flags |= CronExpressionFlag.DayOfMonthLast; - if (Accept(ref pointer, '-')) lastMonthOffset = (byte)ParseNumber(field, ref pointer, 0, field.Last - 1); - if (AcceptCharacter(ref pointer, 'W')) flags |= CronExpressionFlag.NearestWeekday; - return field.AllBits; - } + if (Accept(ref pointer, '-')) lastMonthOffset = (byte)ParseNumber(field, ref pointer, 0, field.Last - 1); + if (AcceptCharacter(ref pointer, 'W')) flags |= CronExpressionFlag.NearestWeekday; + return field.AllBits; + } #if !NET40 - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif - private static unsafe long ParseNthWeekDay(CronField field, ref char* pointer, int dayOfWeek, ref CronExpressionFlag flags, out byte nthDayOfWeek) - { - nthDayOfWeek = (byte)ParseNumber(field, ref pointer, MinNthDayOfWeek, MaxNthDayOfWeek); - flags |= CronExpressionFlag.NthDayOfWeek; - return GetBit(dayOfWeek); - } + private static unsafe long ParseNthWeekDay(CronField field, ref char* pointer, int dayOfWeek, ref CronExpressionFlag flags, out byte nthDayOfWeek) + { + nthDayOfWeek = (byte)ParseNumber(field, ref pointer, MinNthDayOfWeek, MaxNthDayOfWeek); + flags |= CronExpressionFlag.NthDayOfWeek; + return GetBit(dayOfWeek); + } #if !NET40 - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif - private static long ParseLastWeekDay(int dayOfWeek, ref CronExpressionFlag flags) - { - flags |= CronExpressionFlag.DayOfWeekLast; - return GetBit(dayOfWeek); - } + private static long ParseLastWeekDay(int dayOfWeek, ref CronExpressionFlag flags) + { + flags |= CronExpressionFlag.DayOfWeekLast; + return GetBit(dayOfWeek); + } #if !NET40 - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif - private static unsafe bool Accept(ref char* pointer, char character) + private static unsafe bool Accept(ref char* pointer, char character) + { + if (*pointer == character) { - if (*pointer == character) - { - pointer++; - return true; - } - - return false; + pointer++; + return true; } + return false; + } + #if !NET40 - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif - private static unsafe bool AcceptCharacter(ref char* pointer, char character) + private static unsafe bool AcceptCharacter(ref char* pointer, char character) + { + if (ToUpper(*pointer) == character) { - if (ToUpper(*pointer) == character) - { - pointer++; - return true; - } - - return false; + pointer++; + return true; } - private static unsafe int ParseNumber(CronField field, ref char* pointer, int low, int high) + return false; + } + + private static unsafe int ParseNumber(CronField field, ref char* pointer, int low, int high) + { + var num = GetNumber(ref pointer, null); + if (num == -1 || num < low || num > high) { - var num = GetNumber(ref pointer, null); - if (num == -1 || num < low || num > high) - { - ThrowFormatException(field, "Value must be a number between {0} and {1} (all inclusive).", low, high); - } - return num; + ThrowFormatException(field, "Value must be a number between {0} and {1} (all inclusive).", low, high); } + return num; + } #if !NET40 - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif - private static unsafe int ParseValue(CronField field, ref char* pointer) + private static unsafe int ParseValue(CronField field, ref char* pointer) + { + var num = GetNumber(ref pointer, field.Names); + if (num == -1 || num < field.First || num > field.Last) { - var num = GetNumber(ref pointer, field.Names); - if (num == -1 || num < field.First || num > field.Last) - { - ThrowFormatException(field, "Value must be a number between {0} and {1} (all inclusive).", field.First, field.Last); - } - return num; + ThrowFormatException(field, "Value must be a number between {0} and {1} (all inclusive).", field.First, field.Last); } + return num; + } #if !NET40 - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif - private static StringBuilder AppendFieldValue(StringBuilder expressionBuilder, CronField field, long fieldValue) - { - if (field.AllBits == fieldValue) return expressionBuilder.Append('*'); + private static StringBuilder AppendFieldValue(StringBuilder expressionBuilder, CronField field, long fieldValue) + { + if (field.AllBits == fieldValue) return expressionBuilder.Append('*'); - // Unset 7 bit for Day of week field because both 0 and 7 stand for Sunday. - if (field == CronField.DaysOfWeek) fieldValue &= ~(1 << field.Last); + // Unset 7 bit for Day of week field because both 0 and 7 stand for Sunday. + if (field == CronField.DaysOfWeek) fieldValue &= ~(1 << field.Last); - for (var i = GetFirstSet(fieldValue); ; i = GetFirstSet(fieldValue >> i << i)) - { - expressionBuilder.Append(i); - if (fieldValue >> ++i == 0) break; - expressionBuilder.Append(','); - } - - return expressionBuilder; + for (var i = GetFirstSet(fieldValue); ; i = GetFirstSet(fieldValue >> i << i)) + { + expressionBuilder.Append(i); + if (fieldValue >> ++i == 0) break; + expressionBuilder.Append(','); } + return expressionBuilder; + } + #if !NET40 - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif - private StringBuilder AppendDayOfMonth(StringBuilder expressionBuilder, int domValue) + private StringBuilder AppendDayOfMonth(StringBuilder expressionBuilder, int domValue) + { + if (HasFlag(CronExpressionFlag.DayOfMonthLast)) { - if (HasFlag(CronExpressionFlag.DayOfMonthLast)) - { - expressionBuilder.Append('L'); - if (_lastMonthOffset != 0) expressionBuilder.Append($"-{_lastMonthOffset}"); - } - else - { - AppendFieldValue(expressionBuilder, CronField.DaysOfMonth, (uint)domValue); - } + expressionBuilder.Append('L'); + if (_lastMonthOffset != 0) expressionBuilder.Append($"-{_lastMonthOffset}"); + } + else + { + AppendFieldValue(expressionBuilder, CronField.DaysOfMonth, (uint)domValue); + } - if (HasFlag(CronExpressionFlag.NearestWeekday)) expressionBuilder.Append('W'); + if (HasFlag(CronExpressionFlag.NearestWeekday)) expressionBuilder.Append('W'); - return expressionBuilder; - } + return expressionBuilder; + } #if !NET40 - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif - private void AppendDayOfWeek(StringBuilder expressionBuilder, int dowValue) - { - AppendFieldValue(expressionBuilder, CronField.DaysOfWeek, dowValue); + private void AppendDayOfWeek(StringBuilder expressionBuilder, int dowValue) + { + AppendFieldValue(expressionBuilder, CronField.DaysOfWeek, dowValue); - if (HasFlag(CronExpressionFlag.DayOfWeekLast)) expressionBuilder.Append('L'); - else if (HasFlag(CronExpressionFlag.NthDayOfWeek)) expressionBuilder.Append($"#{_nthDayOfWeek}"); - } + if (HasFlag(CronExpressionFlag.DayOfWeekLast)) expressionBuilder.Append('L'); + else if (HasFlag(CronExpressionFlag.NthDayOfWeek)) expressionBuilder.Append($"#{_nthDayOfWeek}"); + } #if !NET40 - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif - private static long GetBits(CronField field, int num1, int num2, int step) - { - if (num2 < num1) return GetReversedRangeBits(field, num1, num2, step); - if (step == 1) return (1L << (num2 + 1)) - (1L << num1); + private static long GetBits(CronField field, int num1, int num2, int step) + { + if (num2 < num1) return GetReversedRangeBits(field, num1, num2, step); + if (step == 1) return (1L << (num2 + 1)) - (1L << num1); - return GetRangeBits(num1, num2, step); - } + return GetRangeBits(num1, num2, step); + } #if !NET40 - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif - private static long GetRangeBits(int low, int high, int step) + private static long GetRangeBits(int low, int high, int step) + { + var bits = 0L; + for (var i = low; i <= high; i += step) { - var bits = 0L; - for (var i = low; i <= high; i += step) - { - SetBit(ref bits, i); - } - return bits; + SetBit(ref bits, i); } + return bits; + } #if !NET40 - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif - private static long GetReversedRangeBits(CronField field, int num1, int num2, int step) - { - var high = field.Last; - // Skip one of sundays. - if (field == CronField.DaysOfWeek) high--; + private static long GetReversedRangeBits(CronField field, int num1, int num2, int step) + { + var high = field.Last; + // Skip one of sundays. + if (field == CronField.DaysOfWeek) high--; - var bits = GetRangeBits(num1, high, step); + var bits = GetRangeBits(num1, high, step); - num1 = field.First + step - (high - num1) % step - 1; - return bits | GetRangeBits(num1, num2, step); - } + num1 = field.First + step - (high - num1) % step - 1; + return bits | GetRangeBits(num1, num2, step); + } #if !NET40 - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif - private static long GetBit(int num1) - { - return 1L << num1; - } + private static long GetBit(int num1) + { + return 1L << num1; + } - private static unsafe int GetNumber(ref char* pointer, int[] names) + private static unsafe int GetNumber(ref char* pointer, int[] names) + { + if (IsDigit(*pointer)) { - if (IsDigit(*pointer)) - { - var num = GetNumeric(*pointer++); + var num = GetNumeric(*pointer++); - if (!IsDigit(*pointer)) return num; + if (!IsDigit(*pointer)) return num; - num = num * 10 + GetNumeric(*pointer++); + num = num * 10 + GetNumeric(*pointer++); - if (!IsDigit(*pointer)) return num; - return -1; - } + if (!IsDigit(*pointer)) return num; + return -1; + } - if (names == null) return -1; + if (names == null) return -1; - if (!IsLetter(*pointer)) return -1; - var buffer = ToUpper(*pointer++); + if (!IsLetter(*pointer)) return -1; + var buffer = ToUpper(*pointer++); - if (!IsLetter(*pointer)) return -1; - buffer |= ToUpper(*pointer++) << 8; + if (!IsLetter(*pointer)) return -1; + buffer |= ToUpper(*pointer++) << 8; - if (!IsLetter(*pointer)) return -1; - buffer |= ToUpper(*pointer++) << 16; + if (!IsLetter(*pointer)) return -1; + buffer |= ToUpper(*pointer++) << 16; - var length = names.Length; + var length = names.Length; - for (var i = 0; i < length; i++) + for (var i = 0; i < length; i++) + { + if (buffer == names[i]) { - if (buffer == names[i]) - { - return i; - } + return i; } - - return -1; } - [MethodImpl(MethodImplOptions.NoInlining)] - private static void ThrowFormatException(CronField field, string format, params object[] args) - { - throw new CronFormatException(field, String.Format(format, args)); - } + return -1; + } - [MethodImpl(MethodImplOptions.NoInlining)] - private static void ThrowFormatException(string format, params object[] args) - { - throw new CronFormatException(String.Format(format, args)); - } + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowFormatException(CronField field, string format, params object[] args) + { + throw new CronFormatException(field, String.Format(format, args)); + } - [MethodImpl(MethodImplOptions.NoInlining)] - private static void ThrowFromShouldBeLessThanToException(string fromName, string toName) - { - throw new ArgumentException($"The value of the {fromName} argument should be less than the value of the {toName} argument.", fromName); - } + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowFormatException(string format, params object[] args) + { + throw new CronFormatException(String.Format(format, args)); + } - [MethodImpl(MethodImplOptions.NoInlining)] - private static void ThrowWrongDateTimeKindException(string paramName) - { - throw new ArgumentException("The supplied DateTime must have the Kind property set to Utc", paramName); - } + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowFromShouldBeLessThanToException(string fromName, string toName) + { + throw new ArgumentException($"The value of the {fromName} argument should be less than the value of the {toName} argument.", fromName); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowWrongDateTimeKindException(string paramName) + { + throw new ArgumentException("The supplied DateTime must have the Kind property set to Utc", paramName); + } #if !NET40 - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif - private static bool GetBit(long value, int index) - { - return (value & (1L << index)) != 0; - } + private static bool GetBit(long value, int index) + { + return (value & (1L << index)) != 0; + } #if !NET40 - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif - private static void SetBit(ref long value, int index) - { - value |= 1L << index; - } + private static void SetBit(ref long value, int index) + { + value |= 1L << index; + } #if !NET40 - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif - private static bool IsEndOfString(int code) - { - return code == '\0'; - } + private static bool IsEndOfString(int code) + { + return code == '\0'; + } #if !NET40 - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif - private static bool IsWhiteSpace(int code) - { - return code == '\t' || code == ' '; - } + private static bool IsWhiteSpace(int code) + { + return code == '\t' || code == ' '; + } #if !NET40 - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif - private static bool IsDigit(int code) - { - return code >= 48 && code <= 57; - } + private static bool IsDigit(int code) + { + return code >= 48 && code <= 57; + } #if !NET40 - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif - private static bool IsLetter(int code) - { - return (code >= 65 && code <= 90) || (code >= 97 && code <= 122); - } + private static bool IsLetter(int code) + { + return (code >= 65 && code <= 90) || (code >= 97 && code <= 122); + } #if !NET40 - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif - private static int GetNumeric(int code) - { - return code - 48; - } + private static int GetNumeric(int code) + { + return code - 48; + } #if !NET40 - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif - private static int ToUpper(int code) + private static int ToUpper(int code) + { + if (code >= 97 && code <= 122) { - if (code >= 97 && code <= 122) - { - return code - 32; - } - - return code; + return code - 32; } + + return code; } } diff --git a/src/Foundatio.Extensions.Hosting/Cronos/CronExpressionFlag.cs b/src/Foundatio.Extensions.Hosting/Cronos/CronExpressionFlag.cs index de9d02358..eb39c64df 100644 --- a/src/Foundatio.Extensions.Hosting/Cronos/CronExpressionFlag.cs +++ b/src/Foundatio.Extensions.Hosting/Cronos/CronExpressionFlag.cs @@ -1,17 +1,17 @@ // The MIT License(MIT) -// +// // Copyright (c) 2017 Sergey Odinokov -// +// // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: -// +// // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. -// +// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -22,15 +22,14 @@ using System; -namespace Cronos +namespace Cronos; + +[Flags] +internal enum CronExpressionFlag : byte { - [Flags] - internal enum CronExpressionFlag : byte - { - DayOfMonthLast = 0b00001, - DayOfWeekLast = 0b00010, - Interval = 0b00100, - NearestWeekday = 0b01000, - NthDayOfWeek = 0b10000 - } + DayOfMonthLast = 0b00001, + DayOfWeekLast = 0b00010, + Interval = 0b00100, + NearestWeekday = 0b01000, + NthDayOfWeek = 0b10000 } diff --git a/src/Foundatio.Extensions.Hosting/Cronos/CronField.cs b/src/Foundatio.Extensions.Hosting/Cronos/CronField.cs index abc8f130a..b919460a6 100644 --- a/src/Foundatio.Extensions.Hosting/Cronos/CronField.cs +++ b/src/Foundatio.Extensions.Hosting/Cronos/CronField.cs @@ -1,17 +1,17 @@ // The MIT License(MIT) -// +// // Copyright (c) 2017 Sergey Odinokov -// +// // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: -// +// // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. -// +// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -20,84 +20,83 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -namespace Cronos +namespace Cronos; + +internal sealed class CronField { - internal sealed class CronField + private static readonly string[] MonthNames = { - private static readonly string[] MonthNames = - { - null, "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" - }; + null, "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" + }; - private static readonly string[] DayOfWeekNames = - { - "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN" - }; + private static readonly string[] DayOfWeekNames = + { + "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN" + }; - private static readonly int[] MonthNamesArray = new int[MonthNames.Length]; - private static readonly int[] DayOfWeekNamesArray = new int[DayOfWeekNames.Length]; + private static readonly int[] MonthNamesArray = new int[MonthNames.Length]; + private static readonly int[] DayOfWeekNamesArray = new int[DayOfWeekNames.Length]; - // 0 and 7 are both Sunday, for compatibility reasons. - public static readonly CronField DaysOfWeek = new("Days of week", 0, 7, DayOfWeekNamesArray, false); + // 0 and 7 are both Sunday, for compatibility reasons. + public static readonly CronField DaysOfWeek = new("Days of week", 0, 7, DayOfWeekNamesArray, false); - public static readonly CronField Months = new("Months", 1, 12, MonthNamesArray, false); - public static readonly CronField DaysOfMonth = new("Days of month", 1, 31, null, false); - public static readonly CronField Hours = new("Hours", 0, 23, null, true); - public static readonly CronField Minutes = new("Minutes", 0, 59, null, true); - public static readonly CronField Seconds = new("Seconds", 0, 59, null, true); + public static readonly CronField Months = new("Months", 1, 12, MonthNamesArray, false); + public static readonly CronField DaysOfMonth = new("Days of month", 1, 31, null, false); + public static readonly CronField Hours = new("Hours", 0, 23, null, true); + public static readonly CronField Minutes = new("Minutes", 0, 59, null, true); + public static readonly CronField Seconds = new("Seconds", 0, 59, null, true); - static CronField() + static CronField() + { + for (var i = 1; i < MonthNames.Length; i++) { - for (var i = 1; i < MonthNames.Length; i++) - { - var name = MonthNames[i].ToUpperInvariant(); - var array = new char[3]; - array[0] = name[0]; - array[1] = name[1]; - array[2] = name[2]; + var name = MonthNames[i].ToUpperInvariant(); + var array = new char[3]; + array[0] = name[0]; + array[1] = name[1]; + array[2] = name[2]; - var combined = name[0] | (name[1] << 8) | (name[2] << 16); + var combined = name[0] | (name[1] << 8) | (name[2] << 16); - MonthNamesArray[i] = combined; - } + MonthNamesArray[i] = combined; + } - for (var i = 0; i < DayOfWeekNames.Length; i++) - { - var name = DayOfWeekNames[i].ToUpperInvariant(); - var array = new char[3]; - array[0] = name[0]; - array[1] = name[1]; - array[2] = name[2]; + for (var i = 0; i < DayOfWeekNames.Length; i++) + { + var name = DayOfWeekNames[i].ToUpperInvariant(); + var array = new char[3]; + array[0] = name[0]; + array[1] = name[1]; + array[2] = name[2]; - var combined = name[0] | (name[1] << 8) | (name[2] << 16); + var combined = name[0] | (name[1] << 8) | (name[2] << 16); - DayOfWeekNamesArray[i] = combined; - } + DayOfWeekNamesArray[i] = combined; } + } - public readonly string Name; - public readonly int First; - public readonly int Last; - public readonly int[] Names; - public readonly bool CanDefineInterval; - public readonly long AllBits; + public readonly string Name; + public readonly int First; + public readonly int Last; + public readonly int[] Names; + public readonly bool CanDefineInterval; + public readonly long AllBits; - private CronField(string name, int first, int last, int[] names, bool canDefineInterval) + private CronField(string name, int first, int last, int[] names, bool canDefineInterval) + { + Name = name; + First = first; + Last = last; + Names = names; + CanDefineInterval = canDefineInterval; + for (int i = First; i <= Last; i++) { - Name = name; - First = first; - Last = last; - Names = names; - CanDefineInterval = canDefineInterval; - for (int i = First; i <= Last; i++) - { - AllBits |= (1L << i); - } + AllBits |= (1L << i); } + } - public override string ToString() - { - return Name; - } + public override string ToString() + { + return Name; } } diff --git a/src/Foundatio.Extensions.Hosting/Cronos/CronFormat.cs b/src/Foundatio.Extensions.Hosting/Cronos/CronFormat.cs index e22ae25e4..ed377bdae 100644 --- a/src/Foundatio.Extensions.Hosting/Cronos/CronFormat.cs +++ b/src/Foundatio.Extensions.Hosting/Cronos/CronFormat.cs @@ -1,17 +1,17 @@ // The MIT License(MIT) -// +// // Copyright (c) 2017 Sergey Odinokov -// +// // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: -// +// // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. -// +// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -22,22 +22,21 @@ using System; -namespace Cronos +namespace Cronos; + +/// +/// Defines the cron format options that customize string parsing for . +/// +[Flags] +public enum CronFormat { /// - /// Defines the cron format options that customize string parsing for . + /// Parsing string must contain only 5 fields: minute, hour, day of month, month, day of week. /// - [Flags] - public enum CronFormat - { - /// - /// Parsing string must contain only 5 fields: minute, hour, day of month, month, day of week. - /// - Standard = 0, + Standard = 0, - /// - /// Second field must be specified in parsing string. - /// - IncludeSeconds = 1 - } + /// + /// Second field must be specified in parsing string. + /// + IncludeSeconds = 1 } diff --git a/src/Foundatio.Extensions.Hosting/Cronos/CronFormatException.cs b/src/Foundatio.Extensions.Hosting/Cronos/CronFormatException.cs index 0297bb063..3b92c9aba 100644 --- a/src/Foundatio.Extensions.Hosting/Cronos/CronFormatException.cs +++ b/src/Foundatio.Extensions.Hosting/Cronos/CronFormatException.cs @@ -1,17 +1,17 @@ // The MIT License(MIT) -// +// // Copyright (c) 2017 Sergey Odinokov -// +// // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: -// +// // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. -// +// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -22,26 +22,25 @@ using System; -namespace Cronos +namespace Cronos; + +/// +/// Represents an exception that's thrown, when invalid Cron expression is given. +/// +#if !NETSTANDARD1_0 +[Serializable] +#endif +public class CronFormatException : FormatException { /// - /// Represents an exception that's thrown, when invalid Cron expression is given. + /// Initializes a new instance of the class with + /// the given message. /// -#if !NETSTANDARD1_0 - [Serializable] -#endif - public class CronFormatException : FormatException + public CronFormatException(string message) : base(message) { - /// - /// Initializes a new instance of the class with - /// the given message. - /// - public CronFormatException(string message) : base(message) - { - } + } - internal CronFormatException(CronField field, string message) : this($"{field}: {message}") - { - } + internal CronFormatException(CronField field, string message) : this($"{field}: {message}") + { } } diff --git a/src/Foundatio.Extensions.Hosting/Cronos/TimeZoneHelper.cs b/src/Foundatio.Extensions.Hosting/Cronos/TimeZoneHelper.cs index 86213a173..9e51bd7bb 100644 --- a/src/Foundatio.Extensions.Hosting/Cronos/TimeZoneHelper.cs +++ b/src/Foundatio.Extensions.Hosting/Cronos/TimeZoneHelper.cs @@ -1,17 +1,17 @@ // The MIT License(MIT) -// +// // Copyright (c) 2017 Sergey Odinokov -// +// // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: -// +// // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. -// +// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -22,84 +22,83 @@ using System; -namespace Cronos +namespace Cronos; + +internal static class TimeZoneHelper { - internal static class TimeZoneHelper + // This method is here because .NET Framework, .NET Core and Mono works different when transition from standard time (ST) to + // daylight saving time (DST) happens. + // When DST ends you set the clocks backward. If you are in USA it happens on first sunday of November at 2:00 am. + // So duration from 1:00 am to 2:00 am repeats twice. + // .NET Framework and .NET Core consider backward DST transition as [1:00 DST ->> 2:00 DST)--[1:00 ST --> 2:00 ST]. So 2:00 is not ambiguous, but 1:00 is ambiguous. + // Mono consider backward DST transition as [1:00 DST ->> 2:00 DST]--(1:00 ST --> 2:00 ST]. So 2:00 is ambiguous, but 1:00 is not ambiguous. + // We have to add 1 tick to ambiguousTime to have the same behavior for all frameworks. Thus 1:00 is ambiguous and 2:00 is not ambiguous. + public static bool IsAmbiguousTime(TimeZoneInfo zone, DateTime ambiguousTime) { - // This method is here because .NET Framework, .NET Core and Mono works different when transition from standard time (ST) to - // daylight saving time (DST) happens. - // When DST ends you set the clocks backward. If you are in USA it happens on first sunday of November at 2:00 am. - // So duration from 1:00 am to 2:00 am repeats twice. - // .NET Framework and .NET Core consider backward DST transition as [1:00 DST ->> 2:00 DST)--[1:00 ST --> 2:00 ST]. So 2:00 is not ambiguous, but 1:00 is ambiguous. - // Mono consider backward DST transition as [1:00 DST ->> 2:00 DST]--(1:00 ST --> 2:00 ST]. So 2:00 is ambiguous, but 1:00 is not ambiguous. - // We have to add 1 tick to ambiguousTime to have the same behavior for all frameworks. Thus 1:00 is ambiguous and 2:00 is not ambiguous. - public static bool IsAmbiguousTime(TimeZoneInfo zone, DateTime ambiguousTime) - { - return zone.IsAmbiguousTime(ambiguousTime.AddTicks(1)); - } + return zone.IsAmbiguousTime(ambiguousTime.AddTicks(1)); + } - public static TimeSpan GetDaylightOffset(TimeZoneInfo zone, DateTime ambiguousDateTime) - { - var offsets = GetAmbiguousOffsets(zone, ambiguousDateTime); - var baseOffset = zone.BaseUtcOffset; + public static TimeSpan GetDaylightOffset(TimeZoneInfo zone, DateTime ambiguousDateTime) + { + var offsets = GetAmbiguousOffsets(zone, ambiguousDateTime); + var baseOffset = zone.BaseUtcOffset; - if (offsets[0] != baseOffset) return offsets[0]; + if (offsets[0] != baseOffset) return offsets[0]; - return offsets[1]; - } + return offsets[1]; + } - public static DateTimeOffset GetDaylightTimeStart(TimeZoneInfo zone, DateTime invalidDateTime) + public static DateTimeOffset GetDaylightTimeStart(TimeZoneInfo zone, DateTime invalidDateTime) + { + var dstTransitionDateTime = new DateTime(invalidDateTime.Year, invalidDateTime.Month, invalidDateTime.Day, + invalidDateTime.Hour, invalidDateTime.Minute, 0, 0, invalidDateTime.Kind); + + while (zone.IsInvalidTime(dstTransitionDateTime)) { - var dstTransitionDateTime = new DateTime(invalidDateTime.Year, invalidDateTime.Month, invalidDateTime.Day, - invalidDateTime.Hour, invalidDateTime.Minute, 0, 0, invalidDateTime.Kind); + dstTransitionDateTime = dstTransitionDateTime.AddMinutes(1); + } - while (zone.IsInvalidTime(dstTransitionDateTime)) - { - dstTransitionDateTime = dstTransitionDateTime.AddMinutes(1); - } + var dstOffset = zone.GetUtcOffset(dstTransitionDateTime); - var dstOffset = zone.GetUtcOffset(dstTransitionDateTime); + return new DateTimeOffset(dstTransitionDateTime, dstOffset); + } - return new DateTimeOffset(dstTransitionDateTime, dstOffset); - } + public static DateTimeOffset GetStandardTimeStart(TimeZoneInfo zone, DateTime ambiguousTime, TimeSpan daylightOffset) + { + var dstTransitionEnd = GetDstTransitionEndDateTime(zone, ambiguousTime); - public static DateTimeOffset GetStandardTimeStart(TimeZoneInfo zone, DateTime ambiguousTime, TimeSpan daylightOffset) - { - var dstTransitionEnd = GetDstTransitionEndDateTime(zone, ambiguousTime); + return new DateTimeOffset(dstTransitionEnd, daylightOffset).ToOffset(zone.BaseUtcOffset); + } - return new DateTimeOffset(dstTransitionEnd, daylightOffset).ToOffset(zone.BaseUtcOffset); - } + public static DateTimeOffset GetAmbiguousIntervalEnd(TimeZoneInfo zone, DateTime ambiguousTime) + { + var dstTransitionEnd = GetDstTransitionEndDateTime(zone, ambiguousTime); - public static DateTimeOffset GetAmbiguousIntervalEnd(TimeZoneInfo zone, DateTime ambiguousTime) - { - var dstTransitionEnd = GetDstTransitionEndDateTime(zone, ambiguousTime); + return new DateTimeOffset(dstTransitionEnd, zone.BaseUtcOffset); + } - return new DateTimeOffset(dstTransitionEnd, zone.BaseUtcOffset); - } + public static DateTimeOffset GetDaylightTimeEnd(TimeZoneInfo zone, DateTime ambiguousTime, TimeSpan daylightOffset) + { + var daylightTransitionEnd = GetDstTransitionEndDateTime(zone, ambiguousTime); - public static DateTimeOffset GetDaylightTimeEnd(TimeZoneInfo zone, DateTime ambiguousTime, TimeSpan daylightOffset) - { - var daylightTransitionEnd = GetDstTransitionEndDateTime(zone, ambiguousTime); + return new DateTimeOffset(daylightTransitionEnd.AddTicks(-1), daylightOffset); + } - return new DateTimeOffset(daylightTransitionEnd.AddTicks(-1), daylightOffset); - } + private static TimeSpan[] GetAmbiguousOffsets(TimeZoneInfo zone, DateTime ambiguousTime) + { + return zone.GetAmbiguousTimeOffsets(ambiguousTime.AddTicks(1)); + } - private static TimeSpan[] GetAmbiguousOffsets(TimeZoneInfo zone, DateTime ambiguousTime) - { - return zone.GetAmbiguousTimeOffsets(ambiguousTime.AddTicks(1)); - } + private static DateTime GetDstTransitionEndDateTime(TimeZoneInfo zone, DateTime ambiguousDateTime) + { + var dstTransitionDateTime = new DateTime(ambiguousDateTime.Year, ambiguousDateTime.Month, ambiguousDateTime.Day, + ambiguousDateTime.Hour, ambiguousDateTime.Minute, 0, 0, ambiguousDateTime.Kind); - private static DateTime GetDstTransitionEndDateTime(TimeZoneInfo zone, DateTime ambiguousDateTime) + while (zone.IsAmbiguousTime(dstTransitionDateTime)) { - var dstTransitionDateTime = new DateTime(ambiguousDateTime.Year, ambiguousDateTime.Month, ambiguousDateTime.Day, - ambiguousDateTime.Hour, ambiguousDateTime.Minute, 0, 0, ambiguousDateTime.Kind); - - while (zone.IsAmbiguousTime(dstTransitionDateTime)) - { - dstTransitionDateTime = dstTransitionDateTime.AddMinutes(1); - } - - return dstTransitionDateTime; + dstTransitionDateTime = dstTransitionDateTime.AddMinutes(1); } + + return dstTransitionDateTime; } } diff --git a/src/Foundatio.Extensions.Hosting/Jobs/HostedJobOptions.cs b/src/Foundatio.Extensions.Hosting/Jobs/HostedJobOptions.cs index 46dace23e..64f9d59e9 100644 --- a/src/Foundatio.Extensions.Hosting/Jobs/HostedJobOptions.cs +++ b/src/Foundatio.Extensions.Hosting/Jobs/HostedJobOptions.cs @@ -1,10 +1,9 @@ using Foundatio.Jobs; -namespace Foundatio.Extensions.Hosting.Jobs +namespace Foundatio.Extensions.Hosting.Jobs; + +public class HostedJobOptions : JobOptions { - public class HostedJobOptions : JobOptions - { - public bool WaitForStartupActions { get; set; } - public string CronSchedule { get; set; } - } + public bool WaitForStartupActions { get; set; } + public string CronSchedule { get; set; } } diff --git a/src/Foundatio.Extensions.Hosting/Jobs/HostedJobService.cs b/src/Foundatio.Extensions.Hosting/Jobs/HostedJobService.cs index b845c4407..546894ead 100644 --- a/src/Foundatio.Extensions.Hosting/Jobs/HostedJobService.cs +++ b/src/Foundatio.Extensions.Hosting/Jobs/HostedJobService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading; using System.Threading.Tasks; using Foundatio.Extensions.Hosting.Startup; @@ -8,91 +8,90 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace Foundatio.Extensions.Hosting.Jobs +namespace Foundatio.Extensions.Hosting.Jobs; + +public class HostedJobService : IHostedService, IJobStatus, IDisposable { - public class HostedJobService : IHostedService, IJobStatus, IDisposable - { - private readonly CancellationTokenSource _stoppingCts = new(); - private Task _executingTask; - private readonly IServiceProvider _serviceProvider; - private readonly ILoggerFactory _loggerFactory; - private readonly ILogger _logger; - private readonly HostedJobOptions _jobOptions; - private bool _hasStarted = false; + private readonly CancellationTokenSource _stoppingCts = new(); + private Task _executingTask; + private readonly IServiceProvider _serviceProvider; + private readonly ILoggerFactory _loggerFactory; + private readonly ILogger _logger; + private readonly HostedJobOptions _jobOptions; + private bool _hasStarted = false; - public HostedJobService(IServiceProvider serviceProvider, HostedJobOptions jobOptions, ILoggerFactory loggerFactory) - { - _serviceProvider = serviceProvider; - _loggerFactory = loggerFactory; - _logger = loggerFactory.CreateLogger(); - _jobOptions = jobOptions; + public HostedJobService(IServiceProvider serviceProvider, HostedJobOptions jobOptions, ILoggerFactory loggerFactory) + { + _serviceProvider = serviceProvider; + _loggerFactory = loggerFactory; + _logger = loggerFactory.CreateLogger(); + _jobOptions = jobOptions; - var lifetime = serviceProvider.GetService(); - lifetime?.RegisterHostedJobInstance(this); - } + var lifetime = serviceProvider.GetService(); + lifetime?.RegisterHostedJobInstance(this); + } - private async Task ExecuteAsync(CancellationToken stoppingToken) + private async Task ExecuteAsync(CancellationToken stoppingToken) + { + if (_jobOptions.WaitForStartupActions) { - if (_jobOptions.WaitForStartupActions) + var startupContext = _serviceProvider.GetService(); + if (startupContext != null) { - var startupContext = _serviceProvider.GetService(); - if (startupContext != null) + var result = await startupContext.WaitForStartupAsync(stoppingToken).AnyContext(); + if (!result.Success) { - var result = await startupContext.WaitForStartupAsync(stoppingToken).AnyContext(); - if (!result.Success) - { - _logger.LogError("Unable to start {JobName} job due to startup actions failure", _jobOptions.Name); - return; - } + _logger.LogError("Unable to start {JobName} job due to startup actions failure", _jobOptions.Name); + return; } } + } - var runner = new JobRunner(_jobOptions, _loggerFactory); + var runner = new JobRunner(_jobOptions, _loggerFactory); - try - { - await runner.RunAsync(stoppingToken).AnyContext(); - _stoppingCts.Cancel(); - } - finally - { - _logger.LogInformation("{JobName} job completed", _jobOptions.Name); - } + try + { + await runner.RunAsync(stoppingToken).AnyContext(); + _stoppingCts.Cancel(); } - - public Task StartAsync(CancellationToken cancellationToken) + finally { - _executingTask = ExecuteAsync(_stoppingCts.Token); - _hasStarted = true; - return _executingTask.IsCompleted ? _executingTask : Task.CompletedTask; + _logger.LogInformation("{JobName} job completed", _jobOptions.Name); } + } - public async Task StopAsync(CancellationToken cancellationToken) - { - if (_executingTask == null) - return; + public Task StartAsync(CancellationToken cancellationToken) + { + _executingTask = ExecuteAsync(_stoppingCts.Token); + _hasStarted = true; + return _executingTask.IsCompleted ? _executingTask : Task.CompletedTask; + } - try - { - _stoppingCts.Cancel(); - } - finally - { - await Task.WhenAny(_executingTask, Task.Delay(-1, cancellationToken)).AnyContext(); - } - } + public async Task StopAsync(CancellationToken cancellationToken) + { + if (_executingTask == null) + return; - public void Dispose() + try { _stoppingCts.Cancel(); - _stoppingCts.Dispose(); } - - public bool IsRunning => _hasStarted == false || (_executingTask != null && !_executingTask.IsCompleted); + finally + { + await Task.WhenAny(_executingTask, Task.Delay(-1, cancellationToken)).AnyContext(); + } } - public interface IJobStatus + public void Dispose() { - bool IsRunning { get; } + _stoppingCts.Cancel(); + _stoppingCts.Dispose(); } + + public bool IsRunning => _hasStarted == false || (_executingTask != null && !_executingTask.IsCompleted); +} + +public interface IJobStatus +{ + bool IsRunning { get; } } diff --git a/src/Foundatio.Extensions.Hosting/Jobs/JobHostExtensions.cs b/src/Foundatio.Extensions.Hosting/Jobs/JobHostExtensions.cs index f7bc2df88..43cea720f 100644 --- a/src/Foundatio.Extensions.Hosting/Jobs/JobHostExtensions.cs +++ b/src/Foundatio.Extensions.Hosting/Jobs/JobHostExtensions.cs @@ -5,106 +5,105 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace Foundatio.Extensions.Hosting.Jobs +namespace Foundatio.Extensions.Hosting.Jobs; + +public static class JobHostExtensions { - public static class JobHostExtensions + public static IServiceCollection AddJob(this IServiceCollection services, HostedJobOptions jobOptions) { - public static IServiceCollection AddJob(this IServiceCollection services, HostedJobOptions jobOptions) - { - if (jobOptions.JobFactory == null) - throw new ArgumentNullException(nameof(jobOptions), "jobOptions.JobFactory is required"); - - if (String.IsNullOrEmpty(jobOptions.CronSchedule)) - { - return services.AddTransient(s => new HostedJobService(s, jobOptions, s.GetService())); - } - else - { - if (!services.Any(s => s.ServiceType == typeof(IHostedService) && s.ImplementationType == typeof(ScheduledJobService))) - services.AddTransient(); + if (jobOptions.JobFactory == null) + throw new ArgumentNullException(nameof(jobOptions), "jobOptions.JobFactory is required"); - return services.AddTransient(s => new ScheduledJobRegistration(jobOptions.JobFactory, jobOptions.CronSchedule)); - } + if (String.IsNullOrEmpty(jobOptions.CronSchedule)) + { + return services.AddTransient(s => new HostedJobService(s, jobOptions, s.GetService())); } - - public static IServiceCollection AddJob(this IServiceCollection services, Func jobFactory, HostedJobOptions jobOptions) + else { - if (String.IsNullOrEmpty(jobOptions.CronSchedule)) - { - return services.AddTransient(s => - { - jobOptions.JobFactory = () => jobFactory(s); - - return new HostedJobService(s, jobOptions, s.GetService()); - }); - } - else - { - if (!services.Any(s => s.ServiceType == typeof(IHostedService) && s.ImplementationType == typeof(ScheduledJobService))) - services.AddTransient(); + if (!services.Any(s => s.ServiceType == typeof(IHostedService) && s.ImplementationType == typeof(ScheduledJobService))) + services.AddTransient(); - return services.AddTransient(s => new ScheduledJobRegistration(() => jobFactory(s), jobOptions.CronSchedule)); - } + return services.AddTransient(s => new ScheduledJobRegistration(jobOptions.JobFactory, jobOptions.CronSchedule)); } + } - public static IServiceCollection AddJob(this IServiceCollection services, HostedJobOptions jobOptions) where T : class, IJob + public static IServiceCollection AddJob(this IServiceCollection services, Func jobFactory, HostedJobOptions jobOptions) + { + if (String.IsNullOrEmpty(jobOptions.CronSchedule)) { - services.AddTransient(); - if (String.IsNullOrEmpty(jobOptions.CronSchedule)) + return services.AddTransient(s => { - return services.AddTransient(s => - { - if (jobOptions.JobFactory == null) - jobOptions.JobFactory = s.GetRequiredService; - - return new HostedJobService(s, jobOptions, s.GetService()); - }); - } - else - { - if (!services.Any(s => s.ServiceType == typeof(IHostedService) && s.ImplementationType == typeof(ScheduledJobService))) - services.AddTransient(); + jobOptions.JobFactory = () => jobFactory(s); - return services.AddTransient(s => new ScheduledJobRegistration(jobOptions.JobFactory ?? (s.GetRequiredService), jobOptions.CronSchedule)); - } + return new HostedJobService(s, jobOptions, s.GetService()); + }); } - - public static IServiceCollection AddJob(this IServiceCollection services, bool waitForStartupActions = false) where T : class, IJob + else { - return services.AddJob(o => o.ApplyDefaults().WaitForStartupActions(waitForStartupActions)); - } + if (!services.Any(s => s.ServiceType == typeof(IHostedService) && s.ImplementationType == typeof(ScheduledJobService))) + services.AddTransient(); - public static IServiceCollection AddCronJob(this IServiceCollection services, string cronSchedule) where T : class, IJob - { - return services.AddJob(o => o.CronSchedule(cronSchedule)); + return services.AddTransient(s => new ScheduledJobRegistration(() => jobFactory(s), jobOptions.CronSchedule)); } + } - public static IServiceCollection AddJob(this IServiceCollection services, Action configureJobOptions) where T : class, IJob + public static IServiceCollection AddJob(this IServiceCollection services, HostedJobOptions jobOptions) where T : class, IJob + { + services.AddTransient(); + if (String.IsNullOrEmpty(jobOptions.CronSchedule)) { - var jobOptionsBuilder = new HostedJobOptionsBuilder(); - configureJobOptions?.Invoke(jobOptionsBuilder); - return services.AddJob(jobOptionsBuilder.Target); - } + return services.AddTransient(s => + { + if (jobOptions.JobFactory == null) + jobOptions.JobFactory = s.GetRequiredService; - public static IServiceCollection AddJob(this IServiceCollection services, Action configureJobOptions) - { - var jobOptionsBuilder = new HostedJobOptionsBuilder(); - configureJobOptions?.Invoke(jobOptionsBuilder); - return services.AddJob(jobOptionsBuilder.Target); + return new HostedJobService(s, jobOptions, s.GetService()); + }); } - - public static IServiceCollection AddJob(this IServiceCollection services, Func jobFactory, Action configureJobOptions) + else { - var jobOptionsBuilder = new HostedJobOptionsBuilder(); - configureJobOptions?.Invoke(jobOptionsBuilder); - return services.AddJob(jobFactory, jobOptionsBuilder.Target); - } + if (!services.Any(s => s.ServiceType == typeof(IHostedService) && s.ImplementationType == typeof(ScheduledJobService))) + services.AddTransient(); - public static IServiceCollection AddJobLifetimeService(this IServiceCollection services) - { - services.AddSingleton(); - services.AddSingleton(x => x.GetRequiredService()); - return services; + return services.AddTransient(s => new ScheduledJobRegistration(jobOptions.JobFactory ?? (s.GetRequiredService), jobOptions.CronSchedule)); } } + + public static IServiceCollection AddJob(this IServiceCollection services, bool waitForStartupActions = false) where T : class, IJob + { + return services.AddJob(o => o.ApplyDefaults().WaitForStartupActions(waitForStartupActions)); + } + + public static IServiceCollection AddCronJob(this IServiceCollection services, string cronSchedule) where T : class, IJob + { + return services.AddJob(o => o.CronSchedule(cronSchedule)); + } + + public static IServiceCollection AddJob(this IServiceCollection services, Action configureJobOptions) where T : class, IJob + { + var jobOptionsBuilder = new HostedJobOptionsBuilder(); + configureJobOptions?.Invoke(jobOptionsBuilder); + return services.AddJob(jobOptionsBuilder.Target); + } + + public static IServiceCollection AddJob(this IServiceCollection services, Action configureJobOptions) + { + var jobOptionsBuilder = new HostedJobOptionsBuilder(); + configureJobOptions?.Invoke(jobOptionsBuilder); + return services.AddJob(jobOptionsBuilder.Target); + } + + public static IServiceCollection AddJob(this IServiceCollection services, Func jobFactory, Action configureJobOptions) + { + var jobOptionsBuilder = new HostedJobOptionsBuilder(); + configureJobOptions?.Invoke(jobOptionsBuilder); + return services.AddJob(jobFactory, jobOptionsBuilder.Target); + } + + public static IServiceCollection AddJobLifetimeService(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(x => x.GetRequiredService()); + return services; + } } diff --git a/src/Foundatio.Extensions.Hosting/Jobs/JobOptionsBuilder.cs b/src/Foundatio.Extensions.Hosting/Jobs/JobOptionsBuilder.cs index 6450bcace..05194610a 100644 --- a/src/Foundatio.Extensions.Hosting/Jobs/JobOptionsBuilder.cs +++ b/src/Foundatio.Extensions.Hosting/Jobs/JobOptionsBuilder.cs @@ -1,87 +1,86 @@ using System; using Foundatio.Jobs; -namespace Foundatio.Extensions.Hosting.Jobs +namespace Foundatio.Extensions.Hosting.Jobs; + +public class HostedJobOptionsBuilder { - public class HostedJobOptionsBuilder + public HostedJobOptionsBuilder(HostedJobOptions target = null) { - public HostedJobOptionsBuilder(HostedJobOptions target = null) - { - Target = target ?? new HostedJobOptions(); - } + Target = target ?? new HostedJobOptions(); + } - public HostedJobOptions Target { get; } + public HostedJobOptions Target { get; } - public HostedJobOptionsBuilder ApplyDefaults() where T : IJob - { - Target.ApplyDefaults(); - return this; - } + public HostedJobOptionsBuilder ApplyDefaults() where T : IJob + { + Target.ApplyDefaults(); + return this; + } - public HostedJobOptionsBuilder ApplyDefaults(Type jobType) - { - JobOptions.ApplyDefaults(Target, jobType); - return this; - } + public HostedJobOptionsBuilder ApplyDefaults(Type jobType) + { + JobOptions.ApplyDefaults(Target, jobType); + return this; + } - public HostedJobOptionsBuilder Name(string value) - { - Target.Name = value; - return this; - } + public HostedJobOptionsBuilder Name(string value) + { + Target.Name = value; + return this; + } - public HostedJobOptionsBuilder Description(string value) - { - Target.Description = value; - return this; - } + public HostedJobOptionsBuilder Description(string value) + { + Target.Description = value; + return this; + } - public HostedJobOptionsBuilder JobFactory(Func value) - { - Target.JobFactory = value; - return this; - } + public HostedJobOptionsBuilder JobFactory(Func value) + { + Target.JobFactory = value; + return this; + } - public HostedJobOptionsBuilder RunContinuous(bool value) - { - Target.RunContinuous = value; - return this; - } + public HostedJobOptionsBuilder RunContinuous(bool value) + { + Target.RunContinuous = value; + return this; + } - public HostedJobOptionsBuilder CronSchedule(string value) - { - Target.CronSchedule = value; - return this; - } + public HostedJobOptionsBuilder CronSchedule(string value) + { + Target.CronSchedule = value; + return this; + } - public HostedJobOptionsBuilder Interval(TimeSpan? value) - { - Target.Interval = value; - return this; - } + public HostedJobOptionsBuilder Interval(TimeSpan? value) + { + Target.Interval = value; + return this; + } - public HostedJobOptionsBuilder InitialDelay(TimeSpan? value) - { - Target.InitialDelay = value; - return this; - } + public HostedJobOptionsBuilder InitialDelay(TimeSpan? value) + { + Target.InitialDelay = value; + return this; + } - public HostedJobOptionsBuilder IterationLimit(int value) - { - Target.IterationLimit = value; - return this; - } + public HostedJobOptionsBuilder IterationLimit(int value) + { + Target.IterationLimit = value; + return this; + } - public HostedJobOptionsBuilder InstanceCount(int value) - { - Target.InstanceCount = value; - return this; - } + public HostedJobOptionsBuilder InstanceCount(int value) + { + Target.InstanceCount = value; + return this; + } - public HostedJobOptionsBuilder WaitForStartupActions(bool value) - { - Target.WaitForStartupActions = value; - return this; - } + public HostedJobOptionsBuilder WaitForStartupActions(bool value) + { + Target.WaitForStartupActions = value; + return this; } } diff --git a/src/Foundatio.Extensions.Hosting/Jobs/ScheduledJobRegistration.cs b/src/Foundatio.Extensions.Hosting/Jobs/ScheduledJobRegistration.cs index fc9ab0b8d..89fd57b62 100644 --- a/src/Foundatio.Extensions.Hosting/Jobs/ScheduledJobRegistration.cs +++ b/src/Foundatio.Extensions.Hosting/Jobs/ScheduledJobRegistration.cs @@ -1,17 +1,16 @@ using System; using Foundatio.Jobs; -namespace Foundatio.Extensions.Hosting.Jobs +namespace Foundatio.Extensions.Hosting.Jobs; + +public class ScheduledJobRegistration { - public class ScheduledJobRegistration + public ScheduledJobRegistration(Func jobFactory, string schedule) { - public ScheduledJobRegistration(Func jobFactory, string schedule) - { - JobFactory = jobFactory; - Schedule = schedule; - } - - public Func JobFactory { get; } - public string Schedule { get; } + JobFactory = jobFactory; + Schedule = schedule; } + + public Func JobFactory { get; } + public string Schedule { get; } } diff --git a/src/Foundatio.Extensions.Hosting/Jobs/ScheduledJobService.cs b/src/Foundatio.Extensions.Hosting/Jobs/ScheduledJobService.cs index fd7ae42e7..e4ec631b7 100644 --- a/src/Foundatio.Extensions.Hosting/Jobs/ScheduledJobService.cs +++ b/src/Foundatio.Extensions.Hosting/Jobs/ScheduledJobService.cs @@ -14,142 +14,141 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -namespace Foundatio.Extensions.Hosting.Jobs +namespace Foundatio.Extensions.Hosting.Jobs; + +public class ScheduledJobService : BackgroundService, IJobStatus { - public class ScheduledJobService : BackgroundService, IJobStatus - { - private readonly List _jobs; - private readonly IServiceProvider _serviceProvider; + private readonly List _jobs; + private readonly IServiceProvider _serviceProvider; - public ScheduledJobService(IServiceProvider serviceProvider, ILoggerFactory loggerFactory) - { - _serviceProvider = serviceProvider; - var cacheClient = serviceProvider.GetService() ?? new InMemoryCacheClient(); - _jobs = new List(serviceProvider.GetServices().Select(j => new ScheduledJobRunner(j.JobFactory, j.Schedule, cacheClient, loggerFactory))); + public ScheduledJobService(IServiceProvider serviceProvider, ILoggerFactory loggerFactory) + { + _serviceProvider = serviceProvider; + var cacheClient = serviceProvider.GetService() ?? new InMemoryCacheClient(); + _jobs = new List(serviceProvider.GetServices().Select(j => new ScheduledJobRunner(j.JobFactory, j.Schedule, cacheClient, loggerFactory))); - var lifetime = serviceProvider.GetService(); - lifetime?.RegisterHostedJobInstance(this); - } + var lifetime = serviceProvider.GetService(); + lifetime?.RegisterHostedJobInstance(this); + } - public bool IsRunning { get; private set; } = true; + public bool IsRunning { get; private set; } = true; - protected override async Task ExecuteAsync(CancellationToken stoppingToken) + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + // TODO: Add more logging throughout + var startupContext = _serviceProvider.GetService(); + if (startupContext != null) { - // TODO: Add more logging throughout - var startupContext = _serviceProvider.GetService(); - if (startupContext != null) + var result = await startupContext.WaitForStartupAsync(stoppingToken).AnyContext(); + if (!result.Success) { - var result = await startupContext.WaitForStartupAsync(stoppingToken).AnyContext(); - if (!result.Success) - { - IsRunning = false; - throw new ApplicationException("Failed to wait for startup actions to complete"); - } + IsRunning = false; + throw new ApplicationException("Failed to wait for startup actions to complete"); } + } - while (!stoppingToken.IsCancellationRequested) - { - var jobsToRun = _jobs.Where(j => j.ShouldRun()).ToArray(); + while (!stoppingToken.IsCancellationRequested) + { + var jobsToRun = _jobs.Where(j => j.ShouldRun()).ToArray(); - foreach (var jobToRun in jobsToRun) - await jobToRun.StartAsync(stoppingToken).AnyContext(); + foreach (var jobToRun in jobsToRun) + await jobToRun.StartAsync(stoppingToken).AnyContext(); - // run jobs every minute since that is the lowest resolution of the cron schedule - var now = SystemClock.Now; - var nextMinute = now.AddTicks(TimeSpan.FromMinutes(1).Ticks - (now.Ticks % TimeSpan.FromMinutes(1).Ticks)); - var timeUntilNextMinute = nextMinute.Subtract(SystemClock.Now).Add(TimeSpan.FromMilliseconds(1)); - await Task.Delay(timeUntilNextMinute, stoppingToken).AnyContext(); - } + // run jobs every minute since that is the lowest resolution of the cron schedule + var now = SystemClock.Now; + var nextMinute = now.AddTicks(TimeSpan.FromMinutes(1).Ticks - (now.Ticks % TimeSpan.FromMinutes(1).Ticks)); + var timeUntilNextMinute = nextMinute.Subtract(SystemClock.Now).Add(TimeSpan.FromMilliseconds(1)); + await Task.Delay(timeUntilNextMinute, stoppingToken).AnyContext(); } + } - private class ScheduledJobRunner + private class ScheduledJobRunner + { + private readonly Func _jobFactory; + private readonly CronExpression _cronSchedule; + private readonly ILockProvider _lockProvider; + private readonly ILogger _logger; + private readonly DateTime _baseDate = new(2010, 1, 1); + private string _cacheKeyPrefix; + + public ScheduledJobRunner(Func jobFactory, string schedule, ICacheClient cacheClient, ILoggerFactory loggerFactory = null) { - private readonly Func _jobFactory; - private readonly CronExpression _cronSchedule; - private readonly ILockProvider _lockProvider; - private readonly ILogger _logger; - private readonly DateTime _baseDate = new(2010, 1, 1); - private string _cacheKeyPrefix; - - public ScheduledJobRunner(Func jobFactory, string schedule, ICacheClient cacheClient, ILoggerFactory loggerFactory = null) - { - _jobFactory = jobFactory; - Schedule = schedule; - _logger = loggerFactory?.CreateLogger() ?? NullLogger.Instance; + _jobFactory = jobFactory; + Schedule = schedule; + _logger = loggerFactory?.CreateLogger() ?? NullLogger.Instance; - _cronSchedule = CronExpression.Parse(schedule); - if (_cronSchedule == null) - throw new ArgumentException("Could not parse schedule.", nameof(schedule)); + _cronSchedule = CronExpression.Parse(schedule); + if (_cronSchedule == null) + throw new ArgumentException("Could not parse schedule.", nameof(schedule)); - var interval = TimeSpan.FromDays(1); + var interval = TimeSpan.FromDays(1); - var nextOccurrence = _cronSchedule.GetNextOccurrence(SystemClock.UtcNow); - if (nextOccurrence.HasValue) - { - var nextNextOccurrence = _cronSchedule.GetNextOccurrence(nextOccurrence.Value); - if (nextNextOccurrence.HasValue) - interval = nextNextOccurrence.Value.Subtract(nextOccurrence.Value); - } + var nextOccurrence = _cronSchedule.GetNextOccurrence(SystemClock.UtcNow); + if (nextOccurrence.HasValue) + { + var nextNextOccurrence = _cronSchedule.GetNextOccurrence(nextOccurrence.Value); + if (nextNextOccurrence.HasValue) + interval = nextNextOccurrence.Value.Subtract(nextOccurrence.Value); + } - _lockProvider = new ThrottlingLockProvider(cacheClient, 1, interval.Add(interval)); + _lockProvider = new ThrottlingLockProvider(cacheClient, 1, interval.Add(interval)); - NextRun = _cronSchedule.GetNextOccurrence(SystemClock.UtcNow); - } + NextRun = _cronSchedule.GetNextOccurrence(SystemClock.UtcNow); + } - public string Schedule { get; private set; } - public DateTime? LastRun { get; private set; } - public DateTime? NextRun { get; private set; } - public Task RunTask { get; private set; } + public string Schedule { get; private set; } + public DateTime? LastRun { get; private set; } + public DateTime? NextRun { get; private set; } + public Task RunTask { get; private set; } - public bool ShouldRun() - { - if (!NextRun.HasValue) - return false; + public bool ShouldRun() + { + if (!NextRun.HasValue) + return false; - // not time yet - if (NextRun > SystemClock.UtcNow) - return false; + // not time yet + if (NextRun > SystemClock.UtcNow) + return false; - // check if already run - if (LastRun != null && LastRun.Value == NextRun.Value) - return false; + // check if already run + if (LastRun != null && LastRun.Value == NextRun.Value) + return false; - return true; - } + return true; + } - public Task StartAsync(CancellationToken cancellationToken = default) + public Task StartAsync(CancellationToken cancellationToken = default) + { + // using lock provider in a cluster with a distributed cache implementation keeps cron jobs from running duplicates + // TODO: provide ability to run cron jobs on a per host isolated schedule + return _lockProvider.TryUsingAsync(GetLockKey(NextRun.Value), t => { - // using lock provider in a cluster with a distributed cache implementation keeps cron jobs from running duplicates - // TODO: provide ability to run cron jobs on a per host isolated schedule - return _lockProvider.TryUsingAsync(GetLockKey(NextRun.Value), t => + // start running the job in a thread + RunTask = Task.Factory.StartNew(async () => { - // start running the job in a thread - RunTask = Task.Factory.StartNew(async () => - { - var job = _jobFactory(); - // TODO: Don't calculate job name every time - string jobName = job.GetType().Name; - var result = await _jobFactory().TryRunAsync(cancellationToken).AnyContext(); - // TODO: Should we only set last run on success? Seems like that could be bad. - _logger.LogJobResult(result, jobName); - }, cancellationToken).Unwrap(); - - LastRun = NextRun; - NextRun = _cronSchedule.GetNextOccurrence(SystemClock.UtcNow); - - return Task.CompletedTask; - }, TimeSpan.Zero, TimeSpan.Zero); - } + var job = _jobFactory(); + // TODO: Don't calculate job name every time + string jobName = job.GetType().Name; + var result = await _jobFactory().TryRunAsync(cancellationToken).AnyContext(); + // TODO: Should we only set last run on success? Seems like that could be bad. + _logger.LogJobResult(result, jobName); + }, cancellationToken).Unwrap(); + + LastRun = NextRun; + NextRun = _cronSchedule.GetNextOccurrence(SystemClock.UtcNow); - private string GetLockKey(DateTime date) - { - if (_cacheKeyPrefix == null) - _cacheKeyPrefix = TypeHelper.GetTypeDisplayName(_jobFactory().GetType()); + return Task.CompletedTask; + }, TimeSpan.Zero, TimeSpan.Zero); + } + + private string GetLockKey(DateTime date) + { + if (_cacheKeyPrefix == null) + _cacheKeyPrefix = TypeHelper.GetTypeDisplayName(_jobFactory().GetType()); - long minute = (long)date.Subtract(_baseDate).TotalMinutes; + long minute = (long)date.Subtract(_baseDate).TotalMinutes; - return _cacheKeyPrefix + minute; - } + return _cacheKeyPrefix + minute; } } } diff --git a/src/Foundatio.Extensions.Hosting/Jobs/ShutdownHostIfNoJobsRunningService.cs b/src/Foundatio.Extensions.Hosting/Jobs/ShutdownHostIfNoJobsRunningService.cs index c53541d26..be8cedbc6 100644 --- a/src/Foundatio.Extensions.Hosting/Jobs/ShutdownHostIfNoJobsRunningService.cs +++ b/src/Foundatio.Extensions.Hosting/Jobs/ShutdownHostIfNoJobsRunningService.cs @@ -10,72 +10,71 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -namespace Foundatio.Extensions.Hosting.Jobs +namespace Foundatio.Extensions.Hosting.Jobs; + +public class ShutdownHostIfNoJobsRunningService : IHostedService, IDisposable { - public class ShutdownHostIfNoJobsRunningService : IHostedService, IDisposable + private Timer _timer; + private readonly List _jobs = new(); + private readonly IHostApplicationLifetime _lifetime; + private readonly IServiceProvider _serviceProvider; + private bool _isStarted = false; + private readonly ILogger _logger; + + public ShutdownHostIfNoJobsRunningService(IHostApplicationLifetime applicationLifetime, IServiceProvider serviceProvider, ILogger logger) { - private Timer _timer; - private readonly List _jobs = new(); - private readonly IHostApplicationLifetime _lifetime; - private readonly IServiceProvider _serviceProvider; - private bool _isStarted = false; - private readonly ILogger _logger; + _lifetime = applicationLifetime ?? throw new ArgumentNullException(nameof(applicationLifetime)); + _serviceProvider = serviceProvider; + _logger = logger ?? NullLogger.Instance; - public ShutdownHostIfNoJobsRunningService(IHostApplicationLifetime applicationLifetime, IServiceProvider serviceProvider, ILogger logger) + _lifetime.ApplicationStarted.Register(() => { - _lifetime = applicationLifetime ?? throw new ArgumentNullException(nameof(applicationLifetime)); - _serviceProvider = serviceProvider; - _logger = logger ?? NullLogger.Instance; - - _lifetime.ApplicationStarted.Register(() => - { - _timer = new Timer(e => CheckForShutdown(), null, TimeSpan.FromSeconds(3), TimeSpan.FromSeconds(2)); - }); - } + _timer = new Timer(e => CheckForShutdown(), null, TimeSpan.FromSeconds(3), TimeSpan.FromSeconds(2)); + }); + } - public Task StartAsync(CancellationToken cancellationToken) + public Task StartAsync(CancellationToken cancellationToken) + { + // if there are startup actions, don't allow shutdown to happen until after the startup actions have completed + _ = Task.Run(async () => { - // if there are startup actions, don't allow shutdown to happen until after the startup actions have completed - _ = Task.Run(async () => - { - var startupContext = _serviceProvider.GetService(); - if (startupContext != null) - await startupContext.WaitForStartupAsync(cancellationToken).AnyContext(); + var startupContext = _serviceProvider.GetService(); + if (startupContext != null) + await startupContext.WaitForStartupAsync(cancellationToken).AnyContext(); - _isStarted = true; - }, cancellationToken); + _isStarted = true; + }, cancellationToken); - return Task.CompletedTask; - } + return Task.CompletedTask; + } - public Task StopAsync(CancellationToken cancellationToken) - { - _timer?.Change(Timeout.Infinite, 0); - return Task.CompletedTask; - } + public Task StopAsync(CancellationToken cancellationToken) + { + _timer?.Change(Timeout.Infinite, 0); + return Task.CompletedTask; + } - public void RegisterHostedJobInstance(IJobStatus job) - { - _jobs.Add(job); - } + public void RegisterHostedJobInstance(IJobStatus job) + { + _jobs.Add(job); + } - public void CheckForShutdown() - { - if (!_isStarted) - return; + public void CheckForShutdown() + { + if (!_isStarted) + return; - int runningJobCount = _jobs.Count(s => s.IsRunning); - if (runningJobCount != 0) - return; + int runningJobCount = _jobs.Count(s => s.IsRunning); + if (runningJobCount != 0) + return; - _timer?.Change(Timeout.Infinite, 0); - _logger.LogInformation("Stopping host due to no running jobs"); - _lifetime.StopApplication(); - } + _timer?.Change(Timeout.Infinite, 0); + _logger.LogInformation("Stopping host due to no running jobs"); + _lifetime.StopApplication(); + } - public void Dispose() - { - _timer?.Dispose(); - } + public void Dispose() + { + _timer?.Dispose(); } } diff --git a/src/Foundatio.Extensions.Hosting/Startup/IStartupAction.cs b/src/Foundatio.Extensions.Hosting/Startup/IStartupAction.cs index cb022d3d1..98cd9736b 100644 --- a/src/Foundatio.Extensions.Hosting/Startup/IStartupAction.cs +++ b/src/Foundatio.Extensions.Hosting/Startup/IStartupAction.cs @@ -2,10 +2,9 @@ using System.Threading; using System.Threading.Tasks; -namespace Foundatio.Extensions.Hosting.Startup +namespace Foundatio.Extensions.Hosting.Startup; + +public interface IStartupAction { - public interface IStartupAction - { - Task RunAsync(CancellationToken shutdownToken = default); - } + Task RunAsync(CancellationToken shutdownToken = default); } diff --git a/src/Foundatio.Extensions.Hosting/Startup/RunStartupActionsService.cs b/src/Foundatio.Extensions.Hosting/Startup/RunStartupActionsService.cs index 3cec6e45e..a95c8fcfe 100644 --- a/src/Foundatio.Extensions.Hosting/Startup/RunStartupActionsService.cs +++ b/src/Foundatio.Extensions.Hosting/Startup/RunStartupActionsService.cs @@ -4,23 +4,22 @@ using Foundatio.Utility; using Microsoft.Extensions.Hosting; -namespace Foundatio.Extensions.Hosting.Startup +namespace Foundatio.Extensions.Hosting.Startup; + +public class RunStartupActionsService : BackgroundService { - public class RunStartupActionsService : BackgroundService - { - private readonly StartupActionsContext _startupContext; - private readonly IServiceProvider _serviceProvider; + private readonly StartupActionsContext _startupContext; + private readonly IServiceProvider _serviceProvider; - public RunStartupActionsService(StartupActionsContext startupContext, IServiceProvider serviceProvider) - { - _startupContext = startupContext; - _serviceProvider = serviceProvider; - } + public RunStartupActionsService(StartupActionsContext startupContext, IServiceProvider serviceProvider) + { + _startupContext = startupContext; + _serviceProvider = serviceProvider; + } - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - var result = await _serviceProvider.RunStartupActionsAsync(stoppingToken).AnyContext(); - _startupContext.MarkStartupComplete(result); - } + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + var result = await _serviceProvider.RunStartupActionsAsync(stoppingToken).AnyContext(); + _startupContext.MarkStartupComplete(result); } } diff --git a/src/Foundatio.Extensions.Hosting/Startup/StartupActionRegistration.cs b/src/Foundatio.Extensions.Hosting/Startup/StartupActionRegistration.cs index c5fd3441a..2380b8358 100644 --- a/src/Foundatio.Extensions.Hosting/Startup/StartupActionRegistration.cs +++ b/src/Foundatio.Extensions.Hosting/Startup/StartupActionRegistration.cs @@ -5,57 +5,56 @@ using Foundatio.Utility; using Microsoft.Extensions.DependencyInjection; -namespace Foundatio.Extensions.Hosting.Startup +namespace Foundatio.Extensions.Hosting.Startup; + +public class StartupActionRegistration { - public class StartupActionRegistration - { - private readonly Func _action; - private readonly Type _actionType; - private static int _currentAutoPriority; + private readonly Func _action; + private readonly Type _actionType; + private static int _currentAutoPriority; - public StartupActionRegistration(string name, Type startupType, int? priority = null) + public StartupActionRegistration(string name, Type startupType, int? priority = null) + { + Name = name; + _actionType = startupType; + if (!priority.HasValue) { - Name = name; - _actionType = startupType; - if (!priority.HasValue) - { - var priorityAttribute = _actionType.GetCustomAttributes(typeof(StartupPriorityAttribute), true).FirstOrDefault() as StartupPriorityAttribute; - Priority = priorityAttribute?.Priority ?? Interlocked.Increment(ref _currentAutoPriority); - } - else - { - Priority = priority.Value; - } + var priorityAttribute = _actionType.GetCustomAttributes(typeof(StartupPriorityAttribute), true).FirstOrDefault() as StartupPriorityAttribute; + Priority = priorityAttribute?.Priority ?? Interlocked.Increment(ref _currentAutoPriority); } - - public StartupActionRegistration(string name, Func action, int? priority = null) + else { - Name = name; - _action = action; - if (!priority.HasValue) - priority = Interlocked.Increment(ref _currentAutoPriority); - Priority = priority.Value; } + } + + public StartupActionRegistration(string name, Func action, int? priority = null) + { + Name = name; + _action = action; + if (!priority.HasValue) + priority = Interlocked.Increment(ref _currentAutoPriority); + + Priority = priority.Value; + } - public string Name { get; private set; } + public string Name { get; private set; } - public int Priority { get; private set; } + public int Priority { get; private set; } - public async Task RunAsync(IServiceProvider serviceProvider, CancellationToken shutdownToken = default) + public async Task RunAsync(IServiceProvider serviceProvider, CancellationToken shutdownToken = default) + { + if (shutdownToken.IsCancellationRequested) + return; + + if (_actionType != null) + { + if (serviceProvider.GetRequiredService(_actionType) is IStartupAction startup) + await startup.RunAsync(shutdownToken).AnyContext(); + } + else { - if (shutdownToken.IsCancellationRequested) - return; - - if (_actionType != null) - { - if (serviceProvider.GetRequiredService(_actionType) is IStartupAction startup) - await startup.RunAsync(shutdownToken).AnyContext(); - } - else - { - await _action(serviceProvider, shutdownToken).AnyContext(); - } + await _action(serviceProvider, shutdownToken).AnyContext(); } } } diff --git a/src/Foundatio.Extensions.Hosting/Startup/StartupActionsContext.cs b/src/Foundatio.Extensions.Hosting/Startup/StartupActionsContext.cs index 5cef575ea..a23ec2003 100644 --- a/src/Foundatio.Extensions.Hosting/Startup/StartupActionsContext.cs +++ b/src/Foundatio.Extensions.Hosting/Startup/StartupActionsContext.cs @@ -4,52 +4,51 @@ using Foundatio.Utility; using Microsoft.Extensions.Logging; -namespace Foundatio.Extensions.Hosting.Startup +namespace Foundatio.Extensions.Hosting.Startup; + +public class StartupActionsContext { - public class StartupActionsContext + private readonly ILogger _logger; + private int _waitCount = 0; + + public StartupActionsContext(ILogger logger) { - private readonly ILogger _logger; - private int _waitCount = 0; + _logger = logger; + } - public StartupActionsContext(ILogger logger) - { - _logger = logger; - } + public bool IsStartupComplete { get; private set; } + public RunStartupActionsResult Result { get; private set; } - public bool IsStartupComplete { get; private set; } - public RunStartupActionsResult Result { get; private set; } + internal void MarkStartupComplete(RunStartupActionsResult result) + { + IsStartupComplete = true; + Result = result; + } - internal void MarkStartupComplete(RunStartupActionsResult result) - { - IsStartupComplete = true; - Result = result; - } + public async Task WaitForStartupAsync(CancellationToken cancellationToken, TimeSpan? maxTimeToWait = null) + { + bool isFirstWaiter = Interlocked.Increment(ref _waitCount) == 1; + var startTime = SystemClock.UtcNow; + var lastStatus = SystemClock.UtcNow; + maxTimeToWait ??= TimeSpan.FromMinutes(5); - public async Task WaitForStartupAsync(CancellationToken cancellationToken, TimeSpan? maxTimeToWait = null) + while (!cancellationToken.IsCancellationRequested && SystemClock.UtcNow.Subtract(startTime) < maxTimeToWait) { - bool isFirstWaiter = Interlocked.Increment(ref _waitCount) == 1; - var startTime = SystemClock.UtcNow; - var lastStatus = SystemClock.UtcNow; - maxTimeToWait ??= TimeSpan.FromMinutes(5); + if (IsStartupComplete) + return Result; - while (!cancellationToken.IsCancellationRequested && SystemClock.UtcNow.Subtract(startTime) < maxTimeToWait) + if (isFirstWaiter && SystemClock.UtcNow.Subtract(lastStatus) > TimeSpan.FromSeconds(5) && _logger.IsEnabled(LogLevel.Information)) { - if (IsStartupComplete) - return Result; - - if (isFirstWaiter && SystemClock.UtcNow.Subtract(lastStatus) > TimeSpan.FromSeconds(5) && _logger.IsEnabled(LogLevel.Information)) - { - lastStatus = SystemClock.UtcNow; - _logger.LogInformation("Waiting for startup actions to be completed for {Duration:mm\\:ss}...", SystemClock.UtcNow.Subtract(startTime)); - } - - await Task.Delay(1000, cancellationToken).AnyContext(); + lastStatus = SystemClock.UtcNow; + _logger.LogInformation("Waiting for startup actions to be completed for {Duration:mm\\:ss}...", SystemClock.UtcNow.Subtract(startTime)); } - if (isFirstWaiter && _logger.IsEnabled(LogLevel.Error)) - _logger.LogError("Timed out waiting for startup actions to be completed after {Duration:mm\\:ss}", SystemClock.UtcNow.Subtract(startTime)); - - return new RunStartupActionsResult { Success = false, ErrorMessage = $"Timed out waiting for startup actions to be completed after {SystemClock.UtcNow.Subtract(startTime):mm\\:ss}" }; + await Task.Delay(1000, cancellationToken).AnyContext(); } + + if (isFirstWaiter && _logger.IsEnabled(LogLevel.Error)) + _logger.LogError("Timed out waiting for startup actions to be completed after {Duration:mm\\:ss}", SystemClock.UtcNow.Subtract(startTime)); + + return new RunStartupActionsResult { Success = false, ErrorMessage = $"Timed out waiting for startup actions to be completed after {SystemClock.UtcNow.Subtract(startTime):mm\\:ss}" }; } } diff --git a/src/Foundatio.Extensions.Hosting/Startup/StartupExtensions.cs b/src/Foundatio.Extensions.Hosting/Startup/StartupExtensions.cs index 8751defcf..fcc71fc3a 100644 --- a/src/Foundatio.Extensions.Hosting/Startup/StartupExtensions.cs +++ b/src/Foundatio.Extensions.Hosting/Startup/StartupExtensions.cs @@ -13,203 +13,202 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -namespace Foundatio.Extensions.Hosting.Startup +namespace Foundatio.Extensions.Hosting.Startup; + +public class RunStartupActionsResult { - public class RunStartupActionsResult - { - public bool Success { get; set; } - public string FailedActionName { get; set; } - public string ErrorMessage { get; set; } - } + public bool Success { get; set; } + public string FailedActionName { get; set; } + public string ErrorMessage { get; set; } +} - public static partial class StartupExtensions +public static partial class StartupExtensions +{ + public static async Task RunStartupActionsAsync(this IServiceProvider serviceProvider, CancellationToken shutdownToken = default) { - public static async Task RunStartupActionsAsync(this IServiceProvider serviceProvider, CancellationToken shutdownToken = default) + using var startupActionsScope = serviceProvider.CreateScope(); + var sw = Stopwatch.StartNew(); + var logger = startupActionsScope.ServiceProvider.GetService()?.CreateLogger("StartupActions") ?? NullLogger.Instance; + var startupActions = startupActionsScope.ServiceProvider.GetServices().ToArray(); + logger.LogInformation("Found {StartupActionCount} registered startup action(s).", startupActions.Length); + + var startupActionPriorityGroups = startupActions.GroupBy(s => s.Priority).OrderBy(s => s.Key).ToArray(); + foreach (var startupActionGroup in startupActionPriorityGroups) { - using var startupActionsScope = serviceProvider.CreateScope(); - var sw = Stopwatch.StartNew(); - var logger = startupActionsScope.ServiceProvider.GetService()?.CreateLogger("StartupActions") ?? NullLogger.Instance; - var startupActions = startupActionsScope.ServiceProvider.GetServices().ToArray(); - logger.LogInformation("Found {StartupActionCount} registered startup action(s).", startupActions.Length); - - var startupActionPriorityGroups = startupActions.GroupBy(s => s.Priority).OrderBy(s => s.Key).ToArray(); - foreach (var startupActionGroup in startupActionPriorityGroups) + int startupActionsCount = startupActionGroup.Count(); + string[] startupActionsNames = startupActionGroup.Select(a => a.Name).ToArray(); + var swGroup = Stopwatch.StartNew(); + string failedActionName = null; + string errorMessage = null; + try { - int startupActionsCount = startupActionGroup.Count(); - string[] startupActionsNames = startupActionGroup.Select(a => a.Name).ToArray(); - var swGroup = Stopwatch.StartNew(); - string failedActionName = null; - string errorMessage = null; - try + if (startupActionsCount == 1) + logger.LogInformation("Running {StartupActions} (priority {Priority}) startup action...", + startupActionsNames, startupActionGroup.Key); + else + logger.LogInformation( + "Running {StartupActions} (priority {Priority}) startup actions in parallel...", + startupActionsNames, startupActionGroup.Key); + + await Task.WhenAll(startupActionGroup.Select(async a => { - if (startupActionsCount == 1) - logger.LogInformation("Running {StartupActions} (priority {Priority}) startup action...", - startupActionsNames, startupActionGroup.Key); - else - logger.LogInformation( - "Running {StartupActions} (priority {Priority}) startup actions in parallel...", - startupActionsNames, startupActionGroup.Key); - - await Task.WhenAll(startupActionGroup.Select(async a => + try { - try - { - // ReSharper disable once AccessToDisposedClosure - await a.RunAsync(startupActionsScope.ServiceProvider, shutdownToken).AnyContext(); - } - catch (Exception ex) - { - failedActionName = a.Name; - errorMessage = ex.Message; - logger.LogError(ex, "Error running {StartupAction} startup action: {Message}", a.Name, - ex.Message); - throw; - } - })).AnyContext(); - swGroup.Stop(); - - if (startupActionsCount == 1) - logger.LogInformation("Completed {StartupActions} startup action in {Duration:mm\\:ss}.", - startupActionsNames, swGroup.Elapsed); - else - logger.LogInformation("Completed {StartupActions} startup actions in {Duration:mm\\:ss}.", - startupActionsNames, swGroup.Elapsed); - } - catch - { - return new RunStartupActionsResult + // ReSharper disable once AccessToDisposedClosure + await a.RunAsync(startupActionsScope.ServiceProvider, shutdownToken).AnyContext(); + } + catch (Exception ex) { - Success = false, - FailedActionName = failedActionName, - ErrorMessage = errorMessage - }; - } + failedActionName = a.Name; + errorMessage = ex.Message; + logger.LogError(ex, "Error running {StartupAction} startup action: {Message}", a.Name, + ex.Message); + throw; + } + })).AnyContext(); + swGroup.Stop(); + + if (startupActionsCount == 1) + logger.LogInformation("Completed {StartupActions} startup action in {Duration:mm\\:ss}.", + startupActionsNames, swGroup.Elapsed); + else + logger.LogInformation("Completed {StartupActions} startup actions in {Duration:mm\\:ss}.", + startupActionsNames, swGroup.Elapsed); + } + catch + { + return new RunStartupActionsResult + { + Success = false, + FailedActionName = failedActionName, + ErrorMessage = errorMessage + }; } + } - sw.Stop(); - logger.LogInformation("Completed all {StartupActionCount} startup action(s) in {Duration:mm\\:ss}.", - startupActions.Length, sw.Elapsed); + sw.Stop(); + logger.LogInformation("Completed all {StartupActionCount} startup action(s) in {Duration:mm\\:ss}.", + startupActions.Length, sw.Elapsed); - return new RunStartupActionsResult { Success = true }; - } + return new RunStartupActionsResult { Success = true }; + } - public static void AddStartupAction(this IServiceCollection services, int? priority = null) where T : IStartupAction - { - services.TryAddSingleton(); - if (!services.Any(s => s.ServiceType == typeof(IHostedService) && s.ImplementationType == typeof(RunStartupActionsService))) - services.AddSingleton(); - services.TryAddTransient(typeof(T)); - services.AddTransient(s => new StartupActionRegistration(typeof(T).Name, typeof(T), priority)); - } + public static void AddStartupAction(this IServiceCollection services, int? priority = null) where T : IStartupAction + { + services.TryAddSingleton(); + if (!services.Any(s => s.ServiceType == typeof(IHostedService) && s.ImplementationType == typeof(RunStartupActionsService))) + services.AddSingleton(); + services.TryAddTransient(typeof(T)); + services.AddTransient(s => new StartupActionRegistration(typeof(T).Name, typeof(T), priority)); + } - public static void AddStartupAction(this IServiceCollection services, string name, int? priority = null) where T : IStartupAction - { - services.TryAddSingleton(); - if (!services.Any(s => s.ServiceType == typeof(IHostedService) && s.ImplementationType == typeof(RunStartupActionsService))) - services.AddSingleton(); - services.TryAddTransient(typeof(T)); - services.AddTransient(s => new StartupActionRegistration(name, typeof(T), priority)); - } + public static void AddStartupAction(this IServiceCollection services, string name, int? priority = null) where T : IStartupAction + { + services.TryAddSingleton(); + if (!services.Any(s => s.ServiceType == typeof(IHostedService) && s.ImplementationType == typeof(RunStartupActionsService))) + services.AddSingleton(); + services.TryAddTransient(typeof(T)); + services.AddTransient(s => new StartupActionRegistration(name, typeof(T), priority)); + } - public static void AddStartupAction(this IServiceCollection services, string name, Action action, int? priority = null) - { - services.AddStartupAction(name, ct => action(), priority); - } + public static void AddStartupAction(this IServiceCollection services, string name, Action action, int? priority = null) + { + services.AddStartupAction(name, ct => action(), priority); + } - public static void AddStartupAction(this IServiceCollection services, string name, Action action, int? priority = null) - { - services.AddStartupAction(name, (sp, ct) => action(sp), priority); - } + public static void AddStartupAction(this IServiceCollection services, string name, Action action, int? priority = null) + { + services.AddStartupAction(name, (sp, ct) => action(sp), priority); + } - public static void AddStartupAction(this IServiceCollection services, string name, Action action, int? priority = null) + public static void AddStartupAction(this IServiceCollection services, string name, Action action, int? priority = null) + { + services.TryAddSingleton(); + if (!services.Any(s => s.ServiceType == typeof(IHostedService) && s.ImplementationType == typeof(RunStartupActionsService))) + services.AddSingleton(); + services.AddTransient(s => new StartupActionRegistration(name, (sp, ct) => { - services.TryAddSingleton(); - if (!services.Any(s => s.ServiceType == typeof(IHostedService) && s.ImplementationType == typeof(RunStartupActionsService))) - services.AddSingleton(); - services.AddTransient(s => new StartupActionRegistration(name, (sp, ct) => - { - action(sp, ct); - return Task.CompletedTask; - }, priority)); - } + action(sp, ct); + return Task.CompletedTask; + }, priority)); + } - public static void AddStartupAction(this IServiceCollection services, string name, Func action, int? priority = null) - { - services.AddStartupAction(name, (sp, ct) => action(), priority); - } + public static void AddStartupAction(this IServiceCollection services, string name, Func action, int? priority = null) + { + services.AddStartupAction(name, (sp, ct) => action(), priority); + } - public static void AddStartupAction(this IServiceCollection services, string name, Func action, int? priority = null) - { - services.AddStartupAction(name, (sp, ct) => action(sp), priority); - } + public static void AddStartupAction(this IServiceCollection services, string name, Func action, int? priority = null) + { + services.AddStartupAction(name, (sp, ct) => action(sp), priority); + } - public static void AddStartupAction(this IServiceCollection services, string name, Func action, int? priority = null) - { - services.TryAddSingleton(); - if (!services.Any(s => s.ServiceType == typeof(IHostedService) && s.ImplementationType == typeof(RunStartupActionsService))) - services.AddSingleton(); - services.AddTransient(s => new StartupActionRegistration(name, action, priority)); - } + public static void AddStartupAction(this IServiceCollection services, string name, Func action, int? priority = null) + { + services.TryAddSingleton(); + if (!services.Any(s => s.ServiceType == typeof(IHostedService) && s.ImplementationType == typeof(RunStartupActionsService))) + services.AddSingleton(); + services.AddTransient(s => new StartupActionRegistration(name, action, priority)); + } - public const string CheckForStartupActionsName = "CheckForStartupActions"; - public static IHealthChecksBuilder AddCheckForStartupActions(this IHealthChecksBuilder builder, params string[] tags) - { - return builder.AddCheck(CheckForStartupActionsName, null, tags); - } + public const string CheckForStartupActionsName = "CheckForStartupActions"; + public static IHealthChecksBuilder AddCheckForStartupActions(this IHealthChecksBuilder builder, params string[] tags) + { + return builder.AddCheck(CheckForStartupActionsName, null, tags); + } - public static IApplicationBuilder UseWaitForStartupActionsBeforeServingRequests(this IApplicationBuilder builder) - { - return builder.UseMiddleware(); - } + public static IApplicationBuilder UseWaitForStartupActionsBeforeServingRequests(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } - public static IApplicationBuilder UseHealthChecks(this IApplicationBuilder builder, string path, params string[] tags) - { - if (tags == null) - tags = Array.Empty(); + public static IApplicationBuilder UseHealthChecks(this IApplicationBuilder builder, string path, params string[] tags) + { + if (tags == null) + tags = Array.Empty(); - return builder.UseHealthChecks(path, new HealthCheckOptions { Predicate = c => c.Tags.Any(t => tags.Contains(t, StringComparer.OrdinalIgnoreCase)) }); - } + return builder.UseHealthChecks(path, new HealthCheckOptions { Predicate = c => c.Tags.Any(t => tags.Contains(t, StringComparer.OrdinalIgnoreCase)) }); + } + + public static IApplicationBuilder UseReadyHealthChecks(this IApplicationBuilder builder, params string[] tags) + { + if (tags == null) + tags = Array.Empty(); - public static IApplicationBuilder UseReadyHealthChecks(this IApplicationBuilder builder, params string[] tags) + var options = new HealthCheckOptions { - if (tags == null) - tags = Array.Empty(); + Predicate = c => c.Tags.Any(t => tags.Contains(t, StringComparer.OrdinalIgnoreCase)) + }; + return builder.UseHealthChecks("/ready", options); + } - var options = new HealthCheckOptions - { - Predicate = c => c.Tags.Any(t => tags.Contains(t, StringComparer.OrdinalIgnoreCase)) - }; - return builder.UseHealthChecks("/ready", options); - } + public static void AddStartupActionToWaitForHealthChecks(this IServiceCollection services, params string[] tags) + { + if (tags == null) + tags = Array.Empty(); - public static void AddStartupActionToWaitForHealthChecks(this IServiceCollection services, params string[] tags) - { - if (tags == null) - tags = Array.Empty(); + services.AddStartupActionToWaitForHealthChecks(c => c.Tags.Any(t => tags.Contains(t, StringComparer.OrdinalIgnoreCase))); + } - services.AddStartupActionToWaitForHealthChecks(c => c.Tags.Any(t => tags.Contains(t, StringComparer.OrdinalIgnoreCase))); - } + public static void AddStartupActionToWaitForHealthChecks(this IServiceCollection services, Func shouldWaitForHealthCheck = null) + { + if (shouldWaitForHealthCheck == null) + shouldWaitForHealthCheck = c => c.Tags.Contains("Critical", StringComparer.OrdinalIgnoreCase); - public static void AddStartupActionToWaitForHealthChecks(this IServiceCollection services, Func shouldWaitForHealthCheck = null) + services.AddStartupAction("WaitForHealthChecks", async (sp, t) => { - if (shouldWaitForHealthCheck == null) - shouldWaitForHealthCheck = c => c.Tags.Contains("Critical", StringComparer.OrdinalIgnoreCase); + if (t.IsCancellationRequested) + return; - services.AddStartupAction("WaitForHealthChecks", async (sp, t) => + var healthCheckService = sp.GetService(); + var logger = sp.GetService()?.CreateLogger("StartupActions") ?? NullLogger.Instance; + var result = await healthCheckService.CheckHealthAsync(c => c.Name != CheckForStartupActionsName && shouldWaitForHealthCheck(c), t).AnyContext(); + while (result.Status == HealthStatus.Unhealthy && !t.IsCancellationRequested) { - if (t.IsCancellationRequested) - return; - - var healthCheckService = sp.GetService(); - var logger = sp.GetService()?.CreateLogger("StartupActions") ?? NullLogger.Instance; - var result = await healthCheckService.CheckHealthAsync(c => c.Name != CheckForStartupActionsName && shouldWaitForHealthCheck(c), t).AnyContext(); - while (result.Status == HealthStatus.Unhealthy && !t.IsCancellationRequested) - { - logger.LogDebug("Last health check was unhealthy. Waiting 1s until next health check"); - await Task.Delay(1000, t).AnyContext(); - result = await healthCheckService.CheckHealthAsync(c => c.Name != CheckForStartupActionsName && shouldWaitForHealthCheck(c), t).AnyContext(); - } - }, -100); - } + logger.LogDebug("Last health check was unhealthy. Waiting 1s until next health check"); + await Task.Delay(1000, t).AnyContext(); + result = await healthCheckService.CheckHealthAsync(c => c.Name != CheckForStartupActionsName && shouldWaitForHealthCheck(c), t).AnyContext(); + } + }, -100); } } diff --git a/src/Foundatio.Extensions.Hosting/Startup/StartupHealthcheck.cs b/src/Foundatio.Extensions.Hosting/Startup/StartupHealthcheck.cs index 0da833aa7..e5bcd18a8 100644 --- a/src/Foundatio.Extensions.Hosting/Startup/StartupHealthcheck.cs +++ b/src/Foundatio.Extensions.Hosting/Startup/StartupHealthcheck.cs @@ -4,32 +4,31 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; -namespace Foundatio.Extensions.Hosting.Startup +namespace Foundatio.Extensions.Hosting.Startup; + +public class StartupActionsHealthCheck : IHealthCheck { - public class StartupActionsHealthCheck : IHealthCheck - { - private readonly IServiceProvider _serviceProvider; + private readonly IServiceProvider _serviceProvider; - public StartupActionsHealthCheck(IServiceProvider serviceProvider) - { - _serviceProvider = serviceProvider; - } + public StartupActionsHealthCheck(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } - public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) - { - var startupContext = _serviceProvider.GetService(); + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + var startupContext = _serviceProvider.GetService(); - // no startup actions registered - if (startupContext == null) - return Task.FromResult(HealthCheckResult.Healthy("No startup actions registered")); + // no startup actions registered + if (startupContext == null) + return Task.FromResult(HealthCheckResult.Healthy("No startup actions registered")); - if (startupContext.IsStartupComplete && startupContext.Result.Success) - return Task.FromResult(HealthCheckResult.Healthy("All startup actions completed")); + if (startupContext.IsStartupComplete && startupContext.Result.Success) + return Task.FromResult(HealthCheckResult.Healthy("All startup actions completed")); - if (startupContext.IsStartupComplete && !startupContext.Result.Success) - return Task.FromResult(HealthCheckResult.Unhealthy($"Startup action \"{startupContext.Result.FailedActionName}\" failed to complete: {startupContext.Result.ErrorMessage}")); + if (startupContext.IsStartupComplete && !startupContext.Result.Success) + return Task.FromResult(HealthCheckResult.Unhealthy($"Startup action \"{startupContext.Result.FailedActionName}\" failed to complete: {startupContext.Result.ErrorMessage}")); - return Task.FromResult(HealthCheckResult.Unhealthy("Startup actions have not completed")); - } + return Task.FromResult(HealthCheckResult.Unhealthy("Startup actions have not completed")); } } diff --git a/src/Foundatio.Extensions.Hosting/Startup/StartupPriorityAttribute.cs b/src/Foundatio.Extensions.Hosting/Startup/StartupPriorityAttribute.cs index ec42f31e2..5f0772ab0 100644 --- a/src/Foundatio.Extensions.Hosting/Startup/StartupPriorityAttribute.cs +++ b/src/Foundatio.Extensions.Hosting/Startup/StartupPriorityAttribute.cs @@ -1,14 +1,13 @@ using System; -namespace Foundatio.Extensions.Hosting.Startup +namespace Foundatio.Extensions.Hosting.Startup; + +public class StartupPriorityAttribute : Attribute { - public class StartupPriorityAttribute : Attribute + public StartupPriorityAttribute(int priority) { - public StartupPriorityAttribute(int priority) - { - Priority = priority; - } - - public int Priority { get; private set; } + Priority = priority; } + + public int Priority { get; private set; } } diff --git a/src/Foundatio.Extensions.Hosting/Startup/WaitForStartupActionsBeforeServingRequestsMiddleware.cs b/src/Foundatio.Extensions.Hosting/Startup/WaitForStartupActionsBeforeServingRequestsMiddleware.cs index b51bfa66c..c5078a322 100644 --- a/src/Foundatio.Extensions.Hosting/Startup/WaitForStartupActionsBeforeServingRequestsMiddleware.cs +++ b/src/Foundatio.Extensions.Hosting/Startup/WaitForStartupActionsBeforeServingRequestsMiddleware.cs @@ -5,47 +5,46 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -namespace Foundatio.Extensions.Hosting.Startup +namespace Foundatio.Extensions.Hosting.Startup; + +public class WaitForStartupActionsBeforeServingRequestsMiddleware { - public class WaitForStartupActionsBeforeServingRequestsMiddleware + private readonly IServiceProvider _serviceProvider; + private readonly RequestDelegate _next; + private readonly IHostApplicationLifetime _applicationLifetime; + + public WaitForStartupActionsBeforeServingRequestsMiddleware(IServiceProvider serviceProvider, RequestDelegate next, IHostApplicationLifetime applicationLifetime) + { + _serviceProvider = serviceProvider; + _next = next; + _applicationLifetime = applicationLifetime; + } + + public async Task Invoke(HttpContext httpContext) { - private readonly IServiceProvider _serviceProvider; - private readonly RequestDelegate _next; - private readonly IHostApplicationLifetime _applicationLifetime; + var startupContext = _serviceProvider.GetService(); - public WaitForStartupActionsBeforeServingRequestsMiddleware(IServiceProvider serviceProvider, RequestDelegate next, IHostApplicationLifetime applicationLifetime) + // no startup actions registered + if (startupContext == null) { - _serviceProvider = serviceProvider; - _next = next; - _applicationLifetime = applicationLifetime; + await _next(httpContext).AnyContext(); + return; } - public async Task Invoke(HttpContext httpContext) + if (startupContext.IsStartupComplete && startupContext.Result.Success) { - var startupContext = _serviceProvider.GetService(); - - // no startup actions registered - if (startupContext == null) - { - await _next(httpContext).AnyContext(); - return; - } - - if (startupContext.IsStartupComplete && startupContext.Result.Success) - { - await _next(httpContext).AnyContext(); - } - else if (startupContext.IsStartupComplete && !startupContext.Result.Success) - { - // kill the server if the startup actions failed - _applicationLifetime.StopApplication(); - } - else - { - httpContext.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; - httpContext.Response.Headers["Retry-After"] = "10"; - await httpContext.Response.WriteAsync("Service Unavailable").AnyContext(); - } + await _next(httpContext).AnyContext(); + } + else if (startupContext.IsStartupComplete && !startupContext.Result.Success) + { + // kill the server if the startup actions failed + _applicationLifetime.StopApplication(); + } + else + { + httpContext.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; + httpContext.Response.Headers["Retry-After"] = "10"; + await httpContext.Response.WriteAsync("Service Unavailable").AnyContext(); } } } diff --git a/src/Foundatio.JsonNet/JsonNetSerializer.cs b/src/Foundatio.JsonNet/JsonNetSerializer.cs index 0e798885b..83470823c 100644 --- a/src/Foundatio.JsonNet/JsonNetSerializer.cs +++ b/src/Foundatio.JsonNet/JsonNetSerializer.cs @@ -2,29 +2,28 @@ using System.IO; using Newtonsoft.Json; -namespace Foundatio.Serializer +namespace Foundatio.Serializer; + +public class JsonNetSerializer : ITextSerializer { - public class JsonNetSerializer : ITextSerializer - { - private readonly JsonSerializer _serializer; + private readonly JsonSerializer _serializer; - public JsonNetSerializer(JsonSerializerSettings settings = null) - { - _serializer = JsonSerializer.Create(settings ?? new JsonSerializerSettings()); - } + public JsonNetSerializer(JsonSerializerSettings settings = null) + { + _serializer = JsonSerializer.Create(settings ?? new JsonSerializerSettings()); + } - public void Serialize(object data, Stream outputStream) - { - var writer = new JsonTextWriter(new StreamWriter(outputStream)); - _serializer.Serialize(writer, data, data.GetType()); - writer.Flush(); - } + public void Serialize(object data, Stream outputStream) + { + var writer = new JsonTextWriter(new StreamWriter(outputStream)); + _serializer.Serialize(writer, data, data.GetType()); + writer.Flush(); + } - public object Deserialize(Stream inputStream, Type objectType) - { - using var sr = new StreamReader(inputStream); - using var reader = new JsonTextReader(sr); - return _serializer.Deserialize(reader, objectType); - } + public object Deserialize(Stream inputStream, Type objectType) + { + using var sr = new StreamReader(inputStream); + using var reader = new JsonTextReader(sr); + return _serializer.Deserialize(reader, objectType); } } diff --git a/src/Foundatio.MessagePack/MessagePackSerializer.cs b/src/Foundatio.MessagePack/MessagePackSerializer.cs index ca36d53c4..4051060fb 100644 --- a/src/Foundatio.MessagePack/MessagePackSerializer.cs +++ b/src/Foundatio.MessagePack/MessagePackSerializer.cs @@ -3,25 +3,24 @@ using MessagePack; using MessagePack.Resolvers; -namespace Foundatio.Serializer +namespace Foundatio.Serializer; + +public class MessagePackSerializer : ISerializer { - public class MessagePackSerializer : ISerializer - { - private readonly MessagePackSerializerOptions _options; + private readonly MessagePackSerializerOptions _options; - public MessagePackSerializer(MessagePackSerializerOptions options = null) - { - _options = options ?? MessagePackSerializerOptions.Standard.WithResolver(ContractlessStandardResolver.Instance); - } + public MessagePackSerializer(MessagePackSerializerOptions options = null) + { + _options = options ?? MessagePackSerializerOptions.Standard.WithResolver(ContractlessStandardResolver.Instance); + } - public void Serialize(object data, Stream output) - { - MessagePack.MessagePackSerializer.Serialize(data.GetType(), output, data, _options); - } + public void Serialize(object data, Stream output) + { + MessagePack.MessagePackSerializer.Serialize(data.GetType(), output, data, _options); + } - public object Deserialize(Stream input, Type objectType) - { - return MessagePack.MessagePackSerializer.Deserialize(objectType, input, _options); - } + public object Deserialize(Stream input, Type objectType) + { + return MessagePack.MessagePackSerializer.Deserialize(objectType, input, _options); } } diff --git a/src/Foundatio.MetricsNET/MetricsNETClient.cs b/src/Foundatio.MetricsNET/MetricsNETClient.cs index f93e9ad8f..122993a2c 100644 --- a/src/Foundatio.MetricsNET/MetricsNETClient.cs +++ b/src/Foundatio.MetricsNET/MetricsNETClient.cs @@ -1,25 +1,24 @@ using System; using Metrics; -namespace Foundatio.Metrics +namespace Foundatio.Metrics; + +public class MetricsNETClient : IMetricsClient { - public class MetricsNETClient : IMetricsClient + public void Counter(string name, int value = 1) { - public void Counter(string name, int value = 1) - { - Metric.Counter(name, Unit.None).Increment(); - } - - public void Gauge(string name, double value) - { - Metric.Gauge(name, () => value, Unit.None); - } + Metric.Counter(name, Unit.None).Increment(); + } - public void Timer(string name, int milliseconds) - { - Metric.Timer(name, Unit.Calls, SamplingType.SlidingWindow, TimeUnit.Milliseconds).Record(milliseconds, TimeUnit.Milliseconds); - } + public void Gauge(string name, double value) + { + Metric.Gauge(name, () => value, Unit.None); + } - public void Dispose() { } + public void Timer(string name, int milliseconds) + { + Metric.Timer(name, Unit.Calls, SamplingType.SlidingWindow, TimeUnit.Milliseconds).Record(milliseconds, TimeUnit.Milliseconds); } + + public void Dispose() { } } diff --git a/src/Foundatio.TestHarness/Caching/CacheClientTestsBase.cs b/src/Foundatio.TestHarness/Caching/CacheClientTestsBase.cs index 0d2d8fc5e..928eea883 100644 --- a/src/Foundatio.TestHarness/Caching/CacheClientTestsBase.cs +++ b/src/Foundatio.TestHarness/Caching/CacheClientTestsBase.cs @@ -11,1037 +11,1036 @@ using Xunit; using Xunit.Abstractions; -namespace Foundatio.Tests.Caching +namespace Foundatio.Tests.Caching; + +public abstract class CacheClientTestsBase : TestWithLoggingBase { - public abstract class CacheClientTestsBase : TestWithLoggingBase + protected CacheClientTestsBase(ITestOutputHelper output) : base(output) { - protected CacheClientTestsBase(ITestOutputHelper output) : base(output) - { - } - - protected virtual ICacheClient GetCacheClient(bool shouldThrowOnSerializationError = true) - { - return null; - } + } - public virtual async Task CanGetAllAsync() - { - var cache = GetCacheClient(); - if (cache == null) - return; + protected virtual ICacheClient GetCacheClient(bool shouldThrowOnSerializationError = true) + { + return null; + } - using (cache) - { - await cache.RemoveAllAsync(); - - await cache.SetAsync("test1", 1); - await cache.SetAsync("test2", 2); - await cache.SetAsync("test3", 3); - var result = await cache.GetAllAsync(new[] { "test1", "test2", "test3" }); - Assert.NotNull(result); - Assert.Equal(3, result.Count); - Assert.Equal(1, result["test1"].Value); - Assert.Equal(2, result["test2"].Value); - Assert.Equal(3, result["test3"].Value); - - await cache.SetAsync("obj1", new SimpleModel { Data1 = "data 1", Data2 = 1 }); - await cache.SetAsync("obj2", new SimpleModel { Data1 = "data 2", Data2 = 2 }); - await cache.SetAsync("obj3", (SimpleModel)null); - await cache.SetAsync("obj4", new SimpleModel { Data1 = "test 1", Data2 = 4 }); - - var result2 = await cache.GetAllAsync(new[] { "obj1", "obj2", "obj3", "obj4", "obj5" }); - Assert.NotNull(result2); - Assert.Equal(5, result2.Count); - Assert.True(result2["obj3"].IsNull); - Assert.False(result2["obj5"].HasValue); - - var obj4 = result2["obj4"]; - Assert.NotNull(obj4); - Assert.Equal("test 1", obj4.Value.Data1); - - await cache.SetAsync("str1", "string 1"); - await cache.SetAsync("str2", "string 2"); - await cache.SetAsync("str3", (string)null); - var result3 = await cache.GetAllAsync(new[] { "str1", "str2", "str3" }); - Assert.NotNull(result3); - Assert.Equal(3, result3.Count); - } - } + public virtual async Task CanGetAllAsync() + { + var cache = GetCacheClient(); + if (cache == null) + return; - public virtual async Task CanGetAllWithOverlapAsync() + using (cache) { - var cache = GetCacheClient(); - if (cache == null) - return; + await cache.RemoveAllAsync(); + + await cache.SetAsync("test1", 1); + await cache.SetAsync("test2", 2); + await cache.SetAsync("test3", 3); + var result = await cache.GetAllAsync(new[] { "test1", "test2", "test3" }); + Assert.NotNull(result); + Assert.Equal(3, result.Count); + Assert.Equal(1, result["test1"].Value); + Assert.Equal(2, result["test2"].Value); + Assert.Equal(3, result["test3"].Value); + + await cache.SetAsync("obj1", new SimpleModel { Data1 = "data 1", Data2 = 1 }); + await cache.SetAsync("obj2", new SimpleModel { Data1 = "data 2", Data2 = 2 }); + await cache.SetAsync("obj3", (SimpleModel)null); + await cache.SetAsync("obj4", new SimpleModel { Data1 = "test 1", Data2 = 4 }); + + var result2 = await cache.GetAllAsync(new[] { "obj1", "obj2", "obj3", "obj4", "obj5" }); + Assert.NotNull(result2); + Assert.Equal(5, result2.Count); + Assert.True(result2["obj3"].IsNull); + Assert.False(result2["obj5"].HasValue); + + var obj4 = result2["obj4"]; + Assert.NotNull(obj4); + Assert.Equal("test 1", obj4.Value.Data1); + + await cache.SetAsync("str1", "string 1"); + await cache.SetAsync("str2", "string 2"); + await cache.SetAsync("str3", (string)null); + var result3 = await cache.GetAllAsync(new[] { "str1", "str2", "str3" }); + Assert.NotNull(result3); + Assert.Equal(3, result3.Count); + } + } - using (cache) - { - await cache.RemoveAllAsync(); - - await cache.SetAsync("test1", 1.0); - await cache.SetAsync("test2", 2.0); - await cache.SetAsync("test3", 3.0); - await cache.SetAllAsync(new Dictionary { - { "test3", 3.5 }, - { "test4", 4.0 }, - { "test5", 5.0 } - }); + public virtual async Task CanGetAllWithOverlapAsync() + { + var cache = GetCacheClient(); + if (cache == null) + return; - var result = await cache.GetAllAsync(new[] { "test1", "test2", "test3", "test4", "test5" }); - Assert.NotNull(result); - Assert.Equal(5, result.Count); - Assert.Equal(1.0, result["test1"].Value); - Assert.Equal(2.0, result["test2"].Value); - Assert.Equal(3.5, result["test3"].Value); - Assert.Equal(4.0, result["test4"].Value); - Assert.Equal(5.0, result["test5"].Value); - } + using (cache) + { + await cache.RemoveAllAsync(); + + await cache.SetAsync("test1", 1.0); + await cache.SetAsync("test2", 2.0); + await cache.SetAsync("test3", 3.0); + await cache.SetAllAsync(new Dictionary { + { "test3", 3.5 }, + { "test4", 4.0 }, + { "test5", 5.0 } + }); + + var result = await cache.GetAllAsync(new[] { "test1", "test2", "test3", "test4", "test5" }); + Assert.NotNull(result); + Assert.Equal(5, result.Count); + Assert.Equal(1.0, result["test1"].Value); + Assert.Equal(2.0, result["test2"].Value); + Assert.Equal(3.5, result["test3"].Value); + Assert.Equal(4.0, result["test4"].Value); + Assert.Equal(5.0, result["test5"].Value); } + } - public virtual async Task CanSetAsync() - { - var cache = GetCacheClient(); - if (cache == null) - return; + public virtual async Task CanSetAsync() + { + var cache = GetCacheClient(); + if (cache == null) + return; - using (cache) - { - await cache.RemoveAllAsync(); + using (cache) + { + await cache.RemoveAllAsync(); - Assert.Equal(3, await cache.ListAddAsync("set", new List { 1, 1, 2, 3 })); - var result = await cache.GetListAsync("set"); - Assert.NotNull(result); - Assert.Equal(3, result.Value.Count); + Assert.Equal(3, await cache.ListAddAsync("set", new List { 1, 1, 2, 3 })); + var result = await cache.GetListAsync("set"); + Assert.NotNull(result); + Assert.Equal(3, result.Value.Count); - Assert.True(await cache.ListRemoveAsync("set", 1)); - result = await cache.GetListAsync("set"); - Assert.NotNull(result); - Assert.Equal(2, result.Value.Count); - } + Assert.True(await cache.ListRemoveAsync("set", 1)); + result = await cache.GetListAsync("set"); + Assert.NotNull(result); + Assert.Equal(2, result.Value.Count); } + } - public virtual async Task CanSetAndGetValueAsync() - { - var cache = GetCacheClient(); - if (cache == null) - return; + public virtual async Task CanSetAndGetValueAsync() + { + var cache = GetCacheClient(); + if (cache == null) + return; - using (cache) - { - await cache.RemoveAllAsync(); - - Assert.False((await cache.GetAsync("donkey")).HasValue); - Assert.False(await cache.ExistsAsync("donkey")); - - SimpleModel nullable = null; - await cache.SetAsync("nullable", nullable); - var nullCacheValue = await cache.GetAsync("nullable"); - Assert.True(nullCacheValue.HasValue); - Assert.True(nullCacheValue.IsNull); - Assert.True(await cache.ExistsAsync("nullable")); - - int? nullableInt = null; - Assert.False(await cache.ExistsAsync("nullableInt")); - await cache.SetAsync("nullableInt", nullableInt); - var nullIntCacheValue = await cache.GetAsync("nullableInt"); - Assert.True(nullIntCacheValue.HasValue); - Assert.True(nullIntCacheValue.IsNull); - Assert.True(await cache.ExistsAsync("nullableInt")); - - await cache.SetAsync("test", 1); - Assert.Equal(1, (await cache.GetAsync("test")).Value); - - Assert.False(await cache.AddAsync("test", 2)); - Assert.Equal(1, (await cache.GetAsync("test")).Value); - - Assert.True(await cache.ReplaceAsync("test", 2)); - Assert.Equal(2, (await cache.GetAsync("test")).Value); - - Assert.True(await cache.RemoveAsync("test")); - Assert.False((await cache.GetAsync("test")).HasValue); - - Assert.True(await cache.AddAsync("test", 2)); - Assert.Equal(2, (await cache.GetAsync("test")).Value); - - Assert.True(await cache.ReplaceAsync("test", new MyData { Message = "Testing" })); - var result = await cache.GetAsync("test"); - Assert.NotNull(result); - Assert.True(result.HasValue); - Assert.Equal("Testing", result.Value.Message); - } + using (cache) + { + await cache.RemoveAllAsync(); + + Assert.False((await cache.GetAsync("donkey")).HasValue); + Assert.False(await cache.ExistsAsync("donkey")); + + SimpleModel nullable = null; + await cache.SetAsync("nullable", nullable); + var nullCacheValue = await cache.GetAsync("nullable"); + Assert.True(nullCacheValue.HasValue); + Assert.True(nullCacheValue.IsNull); + Assert.True(await cache.ExistsAsync("nullable")); + + int? nullableInt = null; + Assert.False(await cache.ExistsAsync("nullableInt")); + await cache.SetAsync("nullableInt", nullableInt); + var nullIntCacheValue = await cache.GetAsync("nullableInt"); + Assert.True(nullIntCacheValue.HasValue); + Assert.True(nullIntCacheValue.IsNull); + Assert.True(await cache.ExistsAsync("nullableInt")); + + await cache.SetAsync("test", 1); + Assert.Equal(1, (await cache.GetAsync("test")).Value); + + Assert.False(await cache.AddAsync("test", 2)); + Assert.Equal(1, (await cache.GetAsync("test")).Value); + + Assert.True(await cache.ReplaceAsync("test", 2)); + Assert.Equal(2, (await cache.GetAsync("test")).Value); + + Assert.True(await cache.RemoveAsync("test")); + Assert.False((await cache.GetAsync("test")).HasValue); + + Assert.True(await cache.AddAsync("test", 2)); + Assert.Equal(2, (await cache.GetAsync("test")).Value); + + Assert.True(await cache.ReplaceAsync("test", new MyData { Message = "Testing" })); + var result = await cache.GetAsync("test"); + Assert.NotNull(result); + Assert.True(result.HasValue); + Assert.Equal("Testing", result.Value.Message); } + } - public virtual async Task CanAddAsync() - { - var cache = GetCacheClient(); - if (cache == null) - return; + public virtual async Task CanAddAsync() + { + var cache = GetCacheClient(); + if (cache == null) + return; - using (cache) - { - await cache.RemoveAllAsync(); - - const string key = "type-id"; - const string val = "value-should-not-change"; - Assert.False(await cache.ExistsAsync(key)); - Assert.True(await cache.AddAsync(key, val)); - Assert.True(await cache.ExistsAsync(key)); - Assert.Equal(val, (await cache.GetAsync(key)).Value); - - Assert.False(await cache.AddAsync(key, "random value")); - Assert.Equal(val, (await cache.GetAsync(key)).Value); - - // Add a sub key - Assert.True(await cache.AddAsync(key + ":1", "nested")); - Assert.True(await cache.ExistsAsync(key + ":1")); - Assert.Equal("nested", (await cache.GetAsync(key + ":1")).Value); - } + using (cache) + { + await cache.RemoveAllAsync(); + + const string key = "type-id"; + const string val = "value-should-not-change"; + Assert.False(await cache.ExistsAsync(key)); + Assert.True(await cache.AddAsync(key, val)); + Assert.True(await cache.ExistsAsync(key)); + Assert.Equal(val, (await cache.GetAsync(key)).Value); + + Assert.False(await cache.AddAsync(key, "random value")); + Assert.Equal(val, (await cache.GetAsync(key)).Value); + + // Add a sub key + Assert.True(await cache.AddAsync(key + ":1", "nested")); + Assert.True(await cache.ExistsAsync(key + ":1")); + Assert.Equal("nested", (await cache.GetAsync(key + ":1")).Value); } + } - public virtual async Task CanAddConcurrentlyAsync() + public virtual async Task CanAddConcurrentlyAsync() + { + var cache = GetCacheClient(); + if (cache == null) + return; + + using (cache) { - var cache = GetCacheClient(); - if (cache == null) - return; + await cache.RemoveAllAsync(); - using (cache) + string cacheKey = Guid.NewGuid().ToString("N").Substring(10); + long adds = 0; + await Run.InParallelAsync(5, async i => { - await cache.RemoveAllAsync(); - - string cacheKey = Guid.NewGuid().ToString("N").Substring(10); - long adds = 0; - await Run.InParallelAsync(5, async i => - { - if (await cache.AddAsync(cacheKey, i, TimeSpan.FromMinutes(1))) - Interlocked.Increment(ref adds); - }); + if (await cache.AddAsync(cacheKey, i, TimeSpan.FromMinutes(1))) + Interlocked.Increment(ref adds); + }); - Assert.Equal(1, adds); - } + Assert.Equal(1, adds); } + } - public virtual async Task CanGetAsync() - { - var cache = GetCacheClient(); - if (cache == null) - return; + public virtual async Task CanGetAsync() + { + var cache = GetCacheClient(); + if (cache == null) + return; - using (cache) - { - await cache.RemoveAllAsync(); + using (cache) + { + await cache.RemoveAllAsync(); - await cache.SetAsync("test", 1); - var cacheValue = await cache.GetAsync("test"); - Assert.True(cacheValue.HasValue); - Assert.Equal(1L, cacheValue.Value); + await cache.SetAsync("test", 1); + var cacheValue = await cache.GetAsync("test"); + Assert.True(cacheValue.HasValue); + Assert.Equal(1L, cacheValue.Value); - await cache.SetAsync("test", 1); - var cacheValue2 = await cache.GetAsync("test"); - Assert.True(cacheValue2.HasValue); - Assert.Equal(1L, cacheValue2.Value); + await cache.SetAsync("test", 1); + var cacheValue2 = await cache.GetAsync("test"); + Assert.True(cacheValue2.HasValue); + Assert.Equal(1L, cacheValue2.Value); - await cache.SetAsync("test", Int64.MaxValue); - await Assert.ThrowsAnyAsync(async () => - { - var cacheValue3 = await cache.GetAsync("test"); - Assert.False(cacheValue3.HasValue); - }); + await cache.SetAsync("test", Int64.MaxValue); + await Assert.ThrowsAnyAsync(async () => + { + var cacheValue3 = await cache.GetAsync("test"); + Assert.False(cacheValue3.HasValue); + }); - cacheValue = await cache.GetAsync("test"); - Assert.True(cacheValue.HasValue); - Assert.Equal(Int64.MaxValue, cacheValue.Value); - } + cacheValue = await cache.GetAsync("test"); + Assert.True(cacheValue.HasValue); + Assert.Equal(Int64.MaxValue, cacheValue.Value); } + } - public virtual async Task CanTryGetAsync() - { - var cache = GetCacheClient(false); - if (cache == null) - return; + public virtual async Task CanTryGetAsync() + { + var cache = GetCacheClient(false); + if (cache == null) + return; - using (cache) - { - await cache.RemoveAllAsync(); + using (cache) + { + await cache.RemoveAllAsync(); - await cache.SetAsync("test", 1); - var cacheValue = await cache.GetAsync("test"); - Assert.True(cacheValue.HasValue); - Assert.Equal(1L, cacheValue.Value); + await cache.SetAsync("test", 1); + var cacheValue = await cache.GetAsync("test"); + Assert.True(cacheValue.HasValue); + Assert.Equal(1L, cacheValue.Value); - await cache.SetAsync("test", 1); - var cacheValue2 = await cache.GetAsync("test"); - Assert.True(cacheValue2.HasValue); - Assert.Equal(1L, cacheValue2.Value); + await cache.SetAsync("test", 1); + var cacheValue2 = await cache.GetAsync("test"); + Assert.True(cacheValue2.HasValue); + Assert.Equal(1L, cacheValue2.Value); - await cache.SetAsync("test", Int64.MaxValue); - var cacheValue3 = await cache.GetAsync("test"); - Assert.False(cacheValue3.HasValue); + await cache.SetAsync("test", Int64.MaxValue); + var cacheValue3 = await cache.GetAsync("test"); + Assert.False(cacheValue3.HasValue); - cacheValue = await cache.GetAsync("test"); - Assert.True(cacheValue.HasValue); - Assert.Equal(Int64.MaxValue, cacheValue.Value); + cacheValue = await cache.GetAsync("test"); + Assert.True(cacheValue.HasValue); + Assert.Equal(Int64.MaxValue, cacheValue.Value); - await cache.SetAsync("test", new MyData - { - Message = "test" - }); - cacheValue = await cache.GetAsync("test"); - Assert.False(cacheValue.HasValue); - } + await cache.SetAsync("test", new MyData + { + Message = "test" + }); + cacheValue = await cache.GetAsync("test"); + Assert.False(cacheValue.HasValue); } + } - public virtual async Task CanUseScopedCachesAsync() - { - var cache = GetCacheClient(); - if (cache == null) - return; + public virtual async Task CanUseScopedCachesAsync() + { + var cache = GetCacheClient(); + if (cache == null) + return; - using (cache) - { - await cache.RemoveAllAsync(); + using (cache) + { + await cache.RemoveAllAsync(); - var scopedCache1 = new ScopedCacheClient(cache, "scoped1"); - var nestedScopedCache1 = new ScopedCacheClient(scopedCache1, "nested"); - var scopedCache2 = new ScopedCacheClient(cache, "scoped2"); + var scopedCache1 = new ScopedCacheClient(cache, "scoped1"); + var nestedScopedCache1 = new ScopedCacheClient(scopedCache1, "nested"); + var scopedCache2 = new ScopedCacheClient(cache, "scoped2"); - await cache.SetAsync("test", 1); - await scopedCache1.SetAsync("test", 2); - await nestedScopedCache1.SetAsync("test", 3); + await cache.SetAsync("test", 1); + await scopedCache1.SetAsync("test", 2); + await nestedScopedCache1.SetAsync("test", 3); - Assert.Equal(1, (await cache.GetAsync("test")).Value); - Assert.Equal(2, (await scopedCache1.GetAsync("test")).Value); - Assert.Equal(3, (await nestedScopedCache1.GetAsync("test")).Value); + Assert.Equal(1, (await cache.GetAsync("test")).Value); + Assert.Equal(2, (await scopedCache1.GetAsync("test")).Value); + Assert.Equal(3, (await nestedScopedCache1.GetAsync("test")).Value); - Assert.Equal(3, (await scopedCache1.GetAsync("nested:test")).Value); - Assert.Equal(3, (await cache.GetAsync("scoped1:nested:test")).Value); + Assert.Equal(3, (await scopedCache1.GetAsync("nested:test")).Value); + Assert.Equal(3, (await cache.GetAsync("scoped1:nested:test")).Value); - // ensure GetAllAsync returns unscoped keys - Assert.Equal("test", (await scopedCache1.GetAllAsync("test")).Keys.FirstOrDefault()); - Assert.Equal("test", (await nestedScopedCache1.GetAllAsync("test")).Keys.FirstOrDefault()); + // ensure GetAllAsync returns unscoped keys + Assert.Equal("test", (await scopedCache1.GetAllAsync("test")).Keys.FirstOrDefault()); + Assert.Equal("test", (await nestedScopedCache1.GetAllAsync("test")).Keys.FirstOrDefault()); - await scopedCache2.SetAsync("test", 1); + await scopedCache2.SetAsync("test", 1); - int result = await scopedCache1.RemoveByPrefixAsync(String.Empty); - Assert.Equal(2, result); + int result = await scopedCache1.RemoveByPrefixAsync(String.Empty); + Assert.Equal(2, result); - // delete without any matching keys - result = await scopedCache1.RemoveByPrefixAsync(String.Empty); - Assert.Equal(0, result); + // delete without any matching keys + result = await scopedCache1.RemoveByPrefixAsync(String.Empty); + Assert.Equal(0, result); - Assert.False((await scopedCache1.GetAsync("test")).HasValue); - Assert.False((await nestedScopedCache1.GetAsync("test")).HasValue); - Assert.Equal(1, (await cache.GetAsync("test")).Value); - Assert.Equal(1, (await scopedCache2.GetAsync("test")).Value); + Assert.False((await scopedCache1.GetAsync("test")).HasValue); + Assert.False((await nestedScopedCache1.GetAsync("test")).HasValue); + Assert.Equal(1, (await cache.GetAsync("test")).Value); + Assert.Equal(1, (await scopedCache2.GetAsync("test")).Value); - await scopedCache2.RemoveAllAsync(); - Assert.False((await scopedCache1.GetAsync("test")).HasValue); - Assert.False((await nestedScopedCache1.GetAsync("test")).HasValue); - Assert.False((await scopedCache2.GetAsync("test")).HasValue); - Assert.Equal(1, (await cache.GetAsync("test")).Value); + await scopedCache2.RemoveAllAsync(); + Assert.False((await scopedCache1.GetAsync("test")).HasValue); + Assert.False((await nestedScopedCache1.GetAsync("test")).HasValue); + Assert.False((await scopedCache2.GetAsync("test")).HasValue); + Assert.Equal(1, (await cache.GetAsync("test")).Value); - Assert.Equal(0, await scopedCache1.GetAsync("total", 0)); - Assert.Equal(10, await scopedCache1.IncrementAsync("total", 10)); - Assert.Equal(10, await scopedCache1.GetAsync("total", 0)); + Assert.Equal(0, await scopedCache1.GetAsync("total", 0)); + Assert.Equal(10, await scopedCache1.IncrementAsync("total", 10)); + Assert.Equal(10, await scopedCache1.GetAsync("total", 0)); - Assert.Equal(0, await nestedScopedCache1.GetAsync("total", 0)); - Assert.Equal(20, await nestedScopedCache1.IncrementAsync("total", 20)); - Assert.Equal(20, await nestedScopedCache1.GetAsync("total", 0)); - Assert.Equal(1, await nestedScopedCache1.RemoveAllAsync(new[] { "id", "total" })); - Assert.Equal(0, await nestedScopedCache1.GetAsync("total", 0)); + Assert.Equal(0, await nestedScopedCache1.GetAsync("total", 0)); + Assert.Equal(20, await nestedScopedCache1.IncrementAsync("total", 20)); + Assert.Equal(20, await nestedScopedCache1.GetAsync("total", 0)); + Assert.Equal(1, await nestedScopedCache1.RemoveAllAsync(new[] { "id", "total" })); + Assert.Equal(0, await nestedScopedCache1.GetAsync("total", 0)); - Assert.Equal(1, await scopedCache1.RemoveAllAsync(new[] { "id", "total" })); - Assert.Equal(0, await scopedCache1.GetAsync("total", 0)); - } + Assert.Equal(1, await scopedCache1.RemoveAllAsync(new[] { "id", "total" })); + Assert.Equal(0, await scopedCache1.GetAsync("total", 0)); } + } - public virtual async Task CanRemoveByPrefixAsync() - { - var cache = GetCacheClient(); - if (cache == null) - return; + public virtual async Task CanRemoveByPrefixAsync() + { + var cache = GetCacheClient(); + if (cache == null) + return; - using (cache) - { - await cache.RemoveAllAsync(); - - string prefix = "blah:"; - await cache.SetAsync("test", 1); - await cache.SetAsync(prefix + "test", 1); - await cache.SetAsync(prefix + "test2", 4); - Assert.Equal(1, (await cache.GetAsync(prefix + "test")).Value); - Assert.Equal(1, (await cache.GetAsync("test")).Value); - - Assert.Equal(0, await cache.RemoveByPrefixAsync(prefix + ":doesntexist")); - Assert.Equal(2, await cache.RemoveByPrefixAsync(prefix)); - Assert.False((await cache.GetAsync(prefix + "test")).HasValue); - Assert.False((await cache.GetAsync(prefix + "test2")).HasValue); - Assert.Equal(1, (await cache.GetAsync("test")).Value); - - Assert.Equal(1, await cache.RemoveByPrefixAsync(String.Empty)); - } + using (cache) + { + await cache.RemoveAllAsync(); + + string prefix = "blah:"; + await cache.SetAsync("test", 1); + await cache.SetAsync(prefix + "test", 1); + await cache.SetAsync(prefix + "test2", 4); + Assert.Equal(1, (await cache.GetAsync(prefix + "test")).Value); + Assert.Equal(1, (await cache.GetAsync("test")).Value); + + Assert.Equal(0, await cache.RemoveByPrefixAsync(prefix + ":doesntexist")); + Assert.Equal(2, await cache.RemoveByPrefixAsync(prefix)); + Assert.False((await cache.GetAsync(prefix + "test")).HasValue); + Assert.False((await cache.GetAsync(prefix + "test2")).HasValue); + Assert.Equal(1, (await cache.GetAsync("test")).Value); + + Assert.Equal(1, await cache.RemoveByPrefixAsync(String.Empty)); } + } - public virtual async Task CanRemoveByPrefixMultipleEntriesAsync(int count) - { - var cache = GetCacheClient(); - if (cache == null) - return; + public virtual async Task CanRemoveByPrefixMultipleEntriesAsync(int count) + { + var cache = GetCacheClient(); + if (cache == null) + return; - using (cache) - { - await cache.RemoveAllAsync(); - const string prefix = "prefix:"; - await cache.SetAsync("test", 1); + using (cache) + { + await cache.RemoveAllAsync(); + const string prefix = "prefix:"; + await cache.SetAsync("test", 1); - await cache.SetAllAsync(Enumerable.Range(0, count).ToDictionary(i => $"{prefix}test{i}")); + await cache.SetAllAsync(Enumerable.Range(0, count).ToDictionary(i => $"{prefix}test{i}")); - Assert.Equal(1, (await cache.GetAsync($"{prefix}test1")).Value); - Assert.Equal(1, (await cache.GetAsync("test")).Value); + Assert.Equal(1, (await cache.GetAsync($"{prefix}test1")).Value); + Assert.Equal(1, (await cache.GetAsync("test")).Value); - Assert.Equal(0, await cache.RemoveByPrefixAsync($"{prefix}:doesntexist")); - Assert.Equal(count, await cache.RemoveByPrefixAsync(prefix)); - } + Assert.Equal(0, await cache.RemoveByPrefixAsync($"{prefix}:doesntexist")); + Assert.Equal(count, await cache.RemoveByPrefixAsync(prefix)); } + } - public virtual async Task CanSetAndGetObjectAsync() + public virtual async Task CanSetAndGetObjectAsync() + { + var cache = GetCacheClient(); + if (cache == null) + return; + + using (cache) { - var cache = GetCacheClient(); - if (cache == null) - return; + await cache.RemoveAllAsync(); - using (cache) + var dt = DateTimeOffset.Now; + var value = new MyData { - await cache.RemoveAllAsync(); - - var dt = DateTimeOffset.Now; - var value = new MyData - { - Type = "test", - Date = dt, - Message = "Hello World" - }; - await cache.SetAsync("test", value); - value.Type = "modified"; - var cachedValue = await cache.GetAsync("test"); - Assert.NotNull(cachedValue); - Assert.Equal(dt, cachedValue.Value.Date); - Assert.False(value.Equals(cachedValue.Value), "Should not be same reference object"); - Assert.Equal("Hello World", cachedValue.Value.Message); - Assert.Equal("test", cachedValue.Value.Type); - } + Type = "test", + Date = dt, + Message = "Hello World" + }; + await cache.SetAsync("test", value); + value.Type = "modified"; + var cachedValue = await cache.GetAsync("test"); + Assert.NotNull(cachedValue); + Assert.Equal(dt, cachedValue.Value.Date); + Assert.False(value.Equals(cachedValue.Value), "Should not be same reference object"); + Assert.Equal("Hello World", cachedValue.Value.Message); + Assert.Equal("test", cachedValue.Value.Type); } + } - public virtual async Task CanSetExpirationAsync() - { - var cache = GetCacheClient(); - if (cache == null) - return; + public virtual async Task CanSetExpirationAsync() + { + var cache = GetCacheClient(); + if (cache == null) + return; - using (cache) - { - await cache.RemoveAllAsync(); - - var expiresAt = SystemClock.UtcNow.AddMilliseconds(300); - bool success = await cache.SetAsync("test", 1, expiresAt); - Assert.True(success); - success = await cache.SetAsync("test2", 1, expiresAt.AddMilliseconds(100)); - Assert.True(success); - Assert.Equal(1, (await cache.GetAsync("test")).Value); - Assert.True((await cache.GetExpirationAsync("test")).Value < TimeSpan.FromSeconds(1)); - - await SystemClock.SleepAsync(500); - Assert.False((await cache.GetAsync("test")).HasValue); - Assert.Null(await cache.GetExpirationAsync("test")); - Assert.False((await cache.GetAsync("test2")).HasValue); - Assert.Null(await cache.GetExpirationAsync("test2")); - } + using (cache) + { + await cache.RemoveAllAsync(); + + var expiresAt = SystemClock.UtcNow.AddMilliseconds(300); + bool success = await cache.SetAsync("test", 1, expiresAt); + Assert.True(success); + success = await cache.SetAsync("test2", 1, expiresAt.AddMilliseconds(100)); + Assert.True(success); + Assert.Equal(1, (await cache.GetAsync("test")).Value); + Assert.True((await cache.GetExpirationAsync("test")).Value < TimeSpan.FromSeconds(1)); + + await SystemClock.SleepAsync(500); + Assert.False((await cache.GetAsync("test")).HasValue); + Assert.Null(await cache.GetExpirationAsync("test")); + Assert.False((await cache.GetAsync("test2")).HasValue); + Assert.Null(await cache.GetExpirationAsync("test2")); } + } - public virtual async Task CanSetMinMaxExpirationAsync() + public virtual async Task CanSetMinMaxExpirationAsync() + { + var cache = GetCacheClient(); + if (cache == null) + return; + + using (cache) { - var cache = GetCacheClient(); - if (cache == null) - return; + await cache.RemoveAllAsync(); - using (cache) + using (TestSystemClock.Install()) { - await cache.RemoveAllAsync(); - - using (TestSystemClock.Install()) - { - var now = DateTime.UtcNow; - TestSystemClock.SetFrozenTime(now); - - var expires = DateTime.MaxValue - now.AddDays(1); - Assert.True(await cache.SetAsync("test1", 1, expires)); - Assert.False(await cache.SetAsync("test2", 1, DateTime.MinValue)); - Assert.True(await cache.SetAsync("test3", 1, DateTime.MaxValue)); - Assert.True(await cache.SetAsync("test4", 1, DateTime.MaxValue - now.AddDays(-1))); - - Assert.Equal(1, (await cache.GetAsync("test1")).Value); - Assert.InRange((await cache.GetExpirationAsync("test1")).Value, expires.Subtract(TimeSpan.FromSeconds(10)), expires); - - Assert.False(await cache.ExistsAsync("test2")); - Assert.Equal(1, (await cache.GetAsync("test3")).Value); - Assert.False((await cache.GetExpirationAsync("test3")).HasValue); - Assert.Equal(1, (await cache.GetAsync("test4")).Value); - Assert.False((await cache.GetExpirationAsync("test4")).HasValue); - } + var now = DateTime.UtcNow; + TestSystemClock.SetFrozenTime(now); + + var expires = DateTime.MaxValue - now.AddDays(1); + Assert.True(await cache.SetAsync("test1", 1, expires)); + Assert.False(await cache.SetAsync("test2", 1, DateTime.MinValue)); + Assert.True(await cache.SetAsync("test3", 1, DateTime.MaxValue)); + Assert.True(await cache.SetAsync("test4", 1, DateTime.MaxValue - now.AddDays(-1))); + + Assert.Equal(1, (await cache.GetAsync("test1")).Value); + Assert.InRange((await cache.GetExpirationAsync("test1")).Value, expires.Subtract(TimeSpan.FromSeconds(10)), expires); + + Assert.False(await cache.ExistsAsync("test2")); + Assert.Equal(1, (await cache.GetAsync("test3")).Value); + Assert.False((await cache.GetExpirationAsync("test3")).HasValue); + Assert.Equal(1, (await cache.GetAsync("test4")).Value); + Assert.False((await cache.GetExpirationAsync("test4")).HasValue); } } + } - public virtual async Task CanIncrementAsync() - { - var cache = GetCacheClient(); - if (cache == null) - return; + public virtual async Task CanIncrementAsync() + { + var cache = GetCacheClient(); + if (cache == null) + return; - using (cache) - { - await cache.RemoveAllAsync(); + using (cache) + { + await cache.RemoveAllAsync(); - Assert.True(await cache.SetAsync("test", 0)); - Assert.Equal(1, await cache.IncrementAsync("test")); - Assert.Equal(1, await cache.IncrementAsync("test1")); - Assert.Equal(0, await cache.IncrementAsync("test3", 0)); + Assert.True(await cache.SetAsync("test", 0)); + Assert.Equal(1, await cache.IncrementAsync("test")); + Assert.Equal(1, await cache.IncrementAsync("test1")); + Assert.Equal(0, await cache.IncrementAsync("test3", 0)); - // The following is not supported by redis. - if (cache is InMemoryCacheClient) - { - Assert.True(await cache.SetAsync("test2", "stringValue")); - Assert.Equal(1, await cache.IncrementAsync("test2")); - } + // The following is not supported by redis. + if (cache is InMemoryCacheClient) + { + Assert.True(await cache.SetAsync("test2", "stringValue")); + Assert.Equal(1, await cache.IncrementAsync("test2")); } } + } - public virtual async Task CanIncrementAndExpireAsync() - { - var cache = GetCacheClient(); - if (cache == null) - return; + public virtual async Task CanIncrementAndExpireAsync() + { + var cache = GetCacheClient(); + if (cache == null) + return; - using (cache) - { - await cache.RemoveAllAsync(); + using (cache) + { + await cache.RemoveAllAsync(); - bool success = await cache.SetAsync("test", 0); - Assert.True(success); + bool success = await cache.SetAsync("test", 0); + Assert.True(success); - var expiresIn = TimeSpan.FromSeconds(1); - double newVal = await cache.IncrementAsync("test", 1, expiresIn); + var expiresIn = TimeSpan.FromSeconds(1); + double newVal = await cache.IncrementAsync("test", 1, expiresIn); - Assert.Equal(1, newVal); + Assert.Equal(1, newVal); - await SystemClock.SleepAsync(1500); - Assert.False((await cache.GetAsync("test")).HasValue); - } + await SystemClock.SleepAsync(1500); + Assert.False((await cache.GetAsync("test")).HasValue); } + } - public virtual async Task CanReplaceIfEqual() - { - var cache = GetCacheClient(); - if (cache == null) - return; + public virtual async Task CanReplaceIfEqual() + { + var cache = GetCacheClient(); + if (cache == null) + return; - using (cache) - { - await cache.RemoveAllAsync(); - - const string cacheKey = "replace-if-equal"; - Assert.True(await cache.AddAsync(cacheKey, "123")); - var result = await cache.GetAsync(cacheKey); - Assert.NotNull(result); - Assert.Equal("123", result.Value); - Assert.Null(await cache.GetExpirationAsync(cacheKey)); - - Assert.False(await cache.ReplaceIfEqualAsync(cacheKey, "456", "789", TimeSpan.FromHours(1))); - Assert.True(await cache.ReplaceIfEqualAsync(cacheKey, "456", "123", TimeSpan.FromHours(1))); - result = await cache.GetAsync(cacheKey); - Assert.NotNull(result); - Assert.Equal("456", result.Value); - Assert.NotNull(await cache.GetExpirationAsync(cacheKey)); - } + using (cache) + { + await cache.RemoveAllAsync(); + + const string cacheKey = "replace-if-equal"; + Assert.True(await cache.AddAsync(cacheKey, "123")); + var result = await cache.GetAsync(cacheKey); + Assert.NotNull(result); + Assert.Equal("123", result.Value); + Assert.Null(await cache.GetExpirationAsync(cacheKey)); + + Assert.False(await cache.ReplaceIfEqualAsync(cacheKey, "456", "789", TimeSpan.FromHours(1))); + Assert.True(await cache.ReplaceIfEqualAsync(cacheKey, "456", "123", TimeSpan.FromHours(1))); + result = await cache.GetAsync(cacheKey); + Assert.NotNull(result); + Assert.Equal("456", result.Value); + Assert.NotNull(await cache.GetExpirationAsync(cacheKey)); } + } - public virtual async Task CanRemoveIfEqual() - { - var cache = GetCacheClient(); - if (cache == null) - return; + public virtual async Task CanRemoveIfEqual() + { + var cache = GetCacheClient(); + if (cache == null) + return; - using (cache) - { - await cache.RemoveAllAsync(); - - Assert.True(await cache.AddAsync("remove-if-equal", "123")); - var result = await cache.GetAsync("remove-if-equal"); - Assert.NotNull(result); - Assert.Equal("123", result.Value); - - Assert.False(await cache.RemoveIfEqualAsync("remove-if-equal", "789")); - Assert.True(await cache.RemoveIfEqualAsync("remove-if-equal", "123")); - result = await cache.GetAsync("remove-if-equal"); - Assert.NotNull(result); - Assert.False(result.HasValue); - } + using (cache) + { + await cache.RemoveAllAsync(); + + Assert.True(await cache.AddAsync("remove-if-equal", "123")); + var result = await cache.GetAsync("remove-if-equal"); + Assert.NotNull(result); + Assert.Equal("123", result.Value); + + Assert.False(await cache.RemoveIfEqualAsync("remove-if-equal", "789")); + Assert.True(await cache.RemoveIfEqualAsync("remove-if-equal", "123")); + result = await cache.GetAsync("remove-if-equal"); + Assert.NotNull(result); + Assert.False(result.HasValue); } + } - public virtual async Task CanRoundTripLargeNumbersAsync() - { - var cache = GetCacheClient(); - if (cache == null) - return; + public virtual async Task CanRoundTripLargeNumbersAsync() + { + var cache = GetCacheClient(); + if (cache == null) + return; - using (cache) - { - await cache.RemoveAllAsync(); + using (cache) + { + await cache.RemoveAllAsync(); - double value = 2 * 1000 * 1000 * 1000; - Assert.True(await cache.SetAsync("test", value)); - Assert.Equal(value, await cache.GetAsync("test", 0)); + double value = 2 * 1000 * 1000 * 1000; + Assert.True(await cache.SetAsync("test", value)); + Assert.Equal(value, await cache.GetAsync("test", 0)); - var lowerValue = value - 1000; - Assert.Equal(1000, await cache.SetIfLowerAsync("test", lowerValue)); - Assert.Equal(lowerValue, await cache.GetAsync("test", 0)); + var lowerValue = value - 1000; + Assert.Equal(1000, await cache.SetIfLowerAsync("test", lowerValue)); + Assert.Equal(lowerValue, await cache.GetAsync("test", 0)); - Assert.Equal(0, await cache.SetIfLowerAsync("test", value)); - Assert.Equal(lowerValue, await cache.GetAsync("test", 0)); + Assert.Equal(0, await cache.SetIfLowerAsync("test", value)); + Assert.Equal(lowerValue, await cache.GetAsync("test", 0)); - Assert.Equal(1000, await cache.SetIfHigherAsync("test", value)); - Assert.Equal(value, await cache.GetAsync("test", 0)); + Assert.Equal(1000, await cache.SetIfHigherAsync("test", value)); + Assert.Equal(value, await cache.GetAsync("test", 0)); - Assert.Equal(0, await cache.SetIfHigherAsync("test", lowerValue)); - Assert.Equal(value, await cache.GetAsync("test", 0)); - } + Assert.Equal(0, await cache.SetIfHigherAsync("test", lowerValue)); + Assert.Equal(value, await cache.GetAsync("test", 0)); } + } - public virtual async Task CanGetAndSetDateTimeAsync() - { - var cache = GetCacheClient(); - if (cache == null) - return; - - using (cache) - { - await cache.RemoveAllAsync(); - - DateTime value = SystemClock.UtcNow.Floor(TimeSpan.FromSeconds(1)); - long unixTimeValue = value.ToUnixTimeSeconds(); - Assert.True(await cache.SetUnixTimeSecondsAsync("test", value)); - Assert.Equal(unixTimeValue, await cache.GetAsync("test", 0)); - var actual = await cache.GetUnixTimeSecondsAsync("test"); - Assert.Equal(value.Ticks, actual.Ticks); - Assert.Equal(value.Kind, actual.Kind); - - value = SystemClock.Now.Floor(TimeSpan.FromMilliseconds(1)); - unixTimeValue = value.ToUnixTimeMilliseconds(); - Assert.True(await cache.SetUnixTimeMillisecondsAsync("test", value)); - Assert.Equal(unixTimeValue, await cache.GetAsync("test", 0)); - actual = (await cache.GetUnixTimeMillisecondsAsync("test")).ToLocalTime(); - Assert.Equal(value.Ticks, actual.Ticks); - Assert.Equal(value.Kind, actual.Kind); - - value = SystemClock.UtcNow.Floor(TimeSpan.FromMilliseconds(1)); - unixTimeValue = value.ToUnixTimeMilliseconds(); - Assert.True(await cache.SetUnixTimeMillisecondsAsync("test", value)); - Assert.Equal(unixTimeValue, await cache.GetAsync("test", 0)); - actual = await cache.GetUnixTimeMillisecondsAsync("test"); - Assert.Equal(value.Ticks, actual.Ticks); - Assert.Equal(value.Kind, actual.Kind); - - var lowerValue = value - TimeSpan.FromHours(1); - var lowerUnixTimeValue = lowerValue.ToUnixTimeMilliseconds(); - Assert.Equal((long)TimeSpan.FromHours(1).TotalMilliseconds, await cache.SetIfLowerAsync("test", lowerValue)); - Assert.Equal(lowerUnixTimeValue, await cache.GetAsync("test", 0)); - - await cache.RemoveAsync("test"); - - Assert.Equal(unixTimeValue, await cache.SetIfLowerAsync("test", value)); - Assert.Equal(unixTimeValue, await cache.GetAsync("test", 0)); - Assert.Equal(value, await cache.GetUnixTimeMillisecondsAsync("test")); - - Assert.Equal(0, await cache.SetIfLowerAsync("test", value.AddHours(1))); - Assert.Equal(unixTimeValue, await cache.GetAsync("test", 0)); - Assert.Equal(value, await cache.GetUnixTimeMillisecondsAsync("test")); - - await cache.RemoveAsync("test"); - - Assert.Equal(unixTimeValue, await cache.SetIfHigherAsync("test", value)); - Assert.Equal(unixTimeValue, await cache.GetAsync("test", 0)); - Assert.Equal(value, await cache.GetUnixTimeMillisecondsAsync("test")); - - Assert.Equal(0, await cache.SetIfHigherAsync("test", value.AddHours(-1))); - Assert.Equal(unixTimeValue, await cache.GetAsync("test", 0)); - Assert.Equal(value, await cache.GetUnixTimeMillisecondsAsync("test")); - - var higherValue = value + TimeSpan.FromHours(1); - var higherUnixTimeValue = higherValue.ToUnixTimeMilliseconds(); - Assert.Equal((long)TimeSpan.FromHours(1).TotalMilliseconds, await cache.SetIfHigherAsync("test", higherValue)); - Assert.Equal(higherUnixTimeValue, await cache.GetAsync("test", 0)); - Assert.Equal(higherValue, await cache.GetUnixTimeMillisecondsAsync("test")); - } - } + public virtual async Task CanGetAndSetDateTimeAsync() + { + var cache = GetCacheClient(); + if (cache == null) + return; - public virtual async Task CanRoundTripLargeNumbersWithExpirationAsync() + using (cache) { - var cache = GetCacheClient(); - if (cache == null) - return; - - using (cache) - { - await cache.RemoveAllAsync(); - - var minExpiration = TimeSpan.FromHours(1).Add(TimeSpan.FromMinutes(59)).Add(TimeSpan.FromSeconds(55)); - double value = 2 * 1000 * 1000 * 1000; - Assert.True(await cache.SetAsync("test", value, TimeSpan.FromHours(2))); - Assert.Equal(value, await cache.GetAsync("test", 0)); - Assert.InRange((await cache.GetExpirationAsync("test")).Value, minExpiration, TimeSpan.FromHours(2)); - - var lowerValue = value - 1000; - Assert.Equal(1000, await cache.SetIfLowerAsync("test", lowerValue, TimeSpan.FromHours(2))); - Assert.Equal(lowerValue, await cache.GetAsync("test", 0)); - Assert.InRange((await cache.GetExpirationAsync("test")).Value, minExpiration, TimeSpan.FromHours(2)); - - Assert.Equal(0, await cache.SetIfLowerAsync("test", value, TimeSpan.FromHours(2))); - Assert.Equal(lowerValue, await cache.GetAsync("test", 0)); - Assert.InRange((await cache.GetExpirationAsync("test")).Value, minExpiration, TimeSpan.FromHours(2)); - - Assert.Equal(1000, await cache.SetIfHigherAsync("test", value, TimeSpan.FromHours(2))); - Assert.Equal(value, await cache.GetAsync("test", 0)); - Assert.InRange((await cache.GetExpirationAsync("test")).Value, minExpiration, TimeSpan.FromHours(2)); - - Assert.Equal(0, await cache.SetIfHigherAsync("test", lowerValue, TimeSpan.FromHours(2))); - Assert.Equal(value, await cache.GetAsync("test", 0)); - Assert.InRange((await cache.GetExpirationAsync("test")).Value, minExpiration, TimeSpan.FromHours(2)); - } + await cache.RemoveAllAsync(); + + DateTime value = SystemClock.UtcNow.Floor(TimeSpan.FromSeconds(1)); + long unixTimeValue = value.ToUnixTimeSeconds(); + Assert.True(await cache.SetUnixTimeSecondsAsync("test", value)); + Assert.Equal(unixTimeValue, await cache.GetAsync("test", 0)); + var actual = await cache.GetUnixTimeSecondsAsync("test"); + Assert.Equal(value.Ticks, actual.Ticks); + Assert.Equal(value.Kind, actual.Kind); + + value = SystemClock.Now.Floor(TimeSpan.FromMilliseconds(1)); + unixTimeValue = value.ToUnixTimeMilliseconds(); + Assert.True(await cache.SetUnixTimeMillisecondsAsync("test", value)); + Assert.Equal(unixTimeValue, await cache.GetAsync("test", 0)); + actual = (await cache.GetUnixTimeMillisecondsAsync("test")).ToLocalTime(); + Assert.Equal(value.Ticks, actual.Ticks); + Assert.Equal(value.Kind, actual.Kind); + + value = SystemClock.UtcNow.Floor(TimeSpan.FromMilliseconds(1)); + unixTimeValue = value.ToUnixTimeMilliseconds(); + Assert.True(await cache.SetUnixTimeMillisecondsAsync("test", value)); + Assert.Equal(unixTimeValue, await cache.GetAsync("test", 0)); + actual = await cache.GetUnixTimeMillisecondsAsync("test"); + Assert.Equal(value.Ticks, actual.Ticks); + Assert.Equal(value.Kind, actual.Kind); + + var lowerValue = value - TimeSpan.FromHours(1); + var lowerUnixTimeValue = lowerValue.ToUnixTimeMilliseconds(); + Assert.Equal((long)TimeSpan.FromHours(1).TotalMilliseconds, await cache.SetIfLowerAsync("test", lowerValue)); + Assert.Equal(lowerUnixTimeValue, await cache.GetAsync("test", 0)); + + await cache.RemoveAsync("test"); + + Assert.Equal(unixTimeValue, await cache.SetIfLowerAsync("test", value)); + Assert.Equal(unixTimeValue, await cache.GetAsync("test", 0)); + Assert.Equal(value, await cache.GetUnixTimeMillisecondsAsync("test")); + + Assert.Equal(0, await cache.SetIfLowerAsync("test", value.AddHours(1))); + Assert.Equal(unixTimeValue, await cache.GetAsync("test", 0)); + Assert.Equal(value, await cache.GetUnixTimeMillisecondsAsync("test")); + + await cache.RemoveAsync("test"); + + Assert.Equal(unixTimeValue, await cache.SetIfHigherAsync("test", value)); + Assert.Equal(unixTimeValue, await cache.GetAsync("test", 0)); + Assert.Equal(value, await cache.GetUnixTimeMillisecondsAsync("test")); + + Assert.Equal(0, await cache.SetIfHigherAsync("test", value.AddHours(-1))); + Assert.Equal(unixTimeValue, await cache.GetAsync("test", 0)); + Assert.Equal(value, await cache.GetUnixTimeMillisecondsAsync("test")); + + var higherValue = value + TimeSpan.FromHours(1); + var higherUnixTimeValue = higherValue.ToUnixTimeMilliseconds(); + Assert.Equal((long)TimeSpan.FromHours(1).TotalMilliseconds, await cache.SetIfHigherAsync("test", higherValue)); + Assert.Equal(higherUnixTimeValue, await cache.GetAsync("test", 0)); + Assert.Equal(higherValue, await cache.GetUnixTimeMillisecondsAsync("test")); } + } + + public virtual async Task CanRoundTripLargeNumbersWithExpirationAsync() + { + var cache = GetCacheClient(); + if (cache == null) + return; - public virtual async Task CanManageListsAsync() + using (cache) { - var cache = GetCacheClient(); - if (cache == null) - return; + await cache.RemoveAllAsync(); + + var minExpiration = TimeSpan.FromHours(1).Add(TimeSpan.FromMinutes(59)).Add(TimeSpan.FromSeconds(55)); + double value = 2 * 1000 * 1000 * 1000; + Assert.True(await cache.SetAsync("test", value, TimeSpan.FromHours(2))); + Assert.Equal(value, await cache.GetAsync("test", 0)); + Assert.InRange((await cache.GetExpirationAsync("test")).Value, minExpiration, TimeSpan.FromHours(2)); + + var lowerValue = value - 1000; + Assert.Equal(1000, await cache.SetIfLowerAsync("test", lowerValue, TimeSpan.FromHours(2))); + Assert.Equal(lowerValue, await cache.GetAsync("test", 0)); + Assert.InRange((await cache.GetExpirationAsync("test")).Value, minExpiration, TimeSpan.FromHours(2)); + + Assert.Equal(0, await cache.SetIfLowerAsync("test", value, TimeSpan.FromHours(2))); + Assert.Equal(lowerValue, await cache.GetAsync("test", 0)); + Assert.InRange((await cache.GetExpirationAsync("test")).Value, minExpiration, TimeSpan.FromHours(2)); + + Assert.Equal(1000, await cache.SetIfHigherAsync("test", value, TimeSpan.FromHours(2))); + Assert.Equal(value, await cache.GetAsync("test", 0)); + Assert.InRange((await cache.GetExpirationAsync("test")).Value, minExpiration, TimeSpan.FromHours(2)); + + Assert.Equal(0, await cache.SetIfHigherAsync("test", lowerValue, TimeSpan.FromHours(2))); + Assert.Equal(value, await cache.GetAsync("test", 0)); + Assert.InRange((await cache.GetExpirationAsync("test")).Value, minExpiration, TimeSpan.FromHours(2)); + } + } - using (cache) - { - await cache.RemoveAllAsync(); + public virtual async Task CanManageListsAsync() + { + var cache = GetCacheClient(); + if (cache == null) + return; - await Assert.ThrowsAsync(() => cache.ListAddAsync(null, 1)); - await Assert.ThrowsAsync(() => cache.ListAddAsync(String.Empty, 1)); + using (cache) + { + await cache.RemoveAllAsync(); - await Assert.ThrowsAsync(() => cache.ListRemoveAsync(null, 1)); - await Assert.ThrowsAsync(() => cache.ListRemoveAsync(String.Empty, 1)); + await Assert.ThrowsAsync(() => cache.ListAddAsync(null, 1)); + await Assert.ThrowsAsync(() => cache.ListAddAsync(String.Empty, 1)); - await Assert.ThrowsAsync(() => cache.GetListAsync>(null)); - await Assert.ThrowsAsync(() => cache.GetListAsync>(String.Empty)); + await Assert.ThrowsAsync(() => cache.ListRemoveAsync(null, 1)); + await Assert.ThrowsAsync(() => cache.ListRemoveAsync(String.Empty, 1)); - await cache.ListAddAsync("test1", new[] { 1, 2, 3 }); - var result = await cache.GetListAsync("test1"); - Assert.NotNull(result); - Assert.Equal(3, result.Value.Count); + await Assert.ThrowsAsync(() => cache.GetListAsync>(null)); + await Assert.ThrowsAsync(() => cache.GetListAsync>(String.Empty)); - await cache.ListRemoveAsync("test1", new[] { 1, 2, 3 }); - result = await cache.GetListAsync("test1"); - Assert.NotNull(result); - Assert.Empty(result.Value); + await cache.ListAddAsync("test1", new[] { 1, 2, 3 }); + var result = await cache.GetListAsync("test1"); + Assert.NotNull(result); + Assert.Equal(3, result.Value.Count); - // test single strings don't get handled as char arrays - await cache.RemoveAllAsync(); + await cache.ListRemoveAsync("test1", new[] { 1, 2, 3 }); + result = await cache.GetListAsync("test1"); + Assert.NotNull(result); + Assert.Empty(result.Value); - await cache.ListAddAsync("stringlist", "myvalue"); - var stringResult = await cache.GetListAsync("stringlist"); - Assert.Single(stringResult.Value); - Assert.Equal("myvalue", stringResult.Value.First()); + // test single strings don't get handled as char arrays + await cache.RemoveAllAsync(); - await cache.ListRemoveAsync("stringlist", "myvalue"); - stringResult = await cache.GetListAsync("stringlist"); - Assert.Empty(stringResult.Value); + await cache.ListAddAsync("stringlist", "myvalue"); + var stringResult = await cache.GetListAsync("stringlist"); + Assert.Single(stringResult.Value); + Assert.Equal("myvalue", stringResult.Value.First()); - await cache.RemoveAllAsync(); + await cache.ListRemoveAsync("stringlist", "myvalue"); + stringResult = await cache.GetListAsync("stringlist"); + Assert.Empty(stringResult.Value); - await cache.ListAddAsync("test1", 1); - await cache.ListAddAsync("test1", 2); - await cache.ListAddAsync("test1", 3); - result = await cache.GetListAsync("test1"); - Assert.NotNull(result); - Assert.Equal(3, result.Value.Count); + await cache.RemoveAllAsync(); - await cache.ListRemoveAsync("test1", 2); - result = await cache.GetListAsync("test1"); - Assert.NotNull(result); - Assert.Equal(2, result.Value.Count); + await cache.ListAddAsync("test1", 1); + await cache.ListAddAsync("test1", 2); + await cache.ListAddAsync("test1", 3); + result = await cache.GetListAsync("test1"); + Assert.NotNull(result); + Assert.Equal(3, result.Value.Count); - await cache.ListRemoveAsync("test1", 1); - await cache.ListRemoveAsync("test1", 3); - result = await cache.GetListAsync("test1"); - Assert.NotNull(result); - Assert.Empty(result.Value); + await cache.ListRemoveAsync("test1", 2); + result = await cache.GetListAsync("test1"); + Assert.NotNull(result); + Assert.Equal(2, result.Value.Count); - await Assert.ThrowsAnyAsync(async () => - { - await cache.AddAsync("key1", 1); - await cache.ListAddAsync("key1", 1); - }); + await cache.ListRemoveAsync("test1", 1); + await cache.ListRemoveAsync("test1", 3); + result = await cache.GetListAsync("test1"); + Assert.NotNull(result); + Assert.Empty(result.Value); - // test paging through items in list - await cache.ListAddAsync("testpaging", new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 }); - var pagedResult = await cache.GetListAsync("testpaging", 1, 5); - Assert.NotNull(pagedResult); - Assert.Equal(5, pagedResult.Value.Count); - Assert.Equal(pagedResult.Value.ToArray(), new[] { 1, 2, 3, 4, 5 }); - - pagedResult = await cache.GetListAsync("testpaging", 2, 5); - Assert.NotNull(pagedResult); - Assert.Equal(5, pagedResult.Value.Count); - Assert.Equal(pagedResult.Value.ToArray(), new[] { 6, 7, 8, 9, 10 }); - - await cache.ListAddAsync("testpaging", new[] { 21, 22 }); - - pagedResult = await cache.GetListAsync("testpaging", 5, 5); - Assert.NotNull(pagedResult); - Assert.Equal(2, pagedResult.Value.Count); - Assert.Equal(pagedResult.Value.ToArray(), new[] { 21, 22 }); - - await cache.ListRemoveAsync("testpaging", 2); - pagedResult = await cache.GetListAsync("testpaging", 1, 5); - Assert.NotNull(pagedResult); - Assert.Equal(5, pagedResult.Value.Count); - Assert.Equal(pagedResult.Value.ToArray(), new[] { 1, 3, 4, 5, 6 }); - } + await Assert.ThrowsAnyAsync(async () => + { + await cache.AddAsync("key1", 1); + await cache.ListAddAsync("key1", 1); + }); + + // test paging through items in list + await cache.ListAddAsync("testpaging", new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 }); + var pagedResult = await cache.GetListAsync("testpaging", 1, 5); + Assert.NotNull(pagedResult); + Assert.Equal(5, pagedResult.Value.Count); + Assert.Equal(pagedResult.Value.ToArray(), new[] { 1, 2, 3, 4, 5 }); + + pagedResult = await cache.GetListAsync("testpaging", 2, 5); + Assert.NotNull(pagedResult); + Assert.Equal(5, pagedResult.Value.Count); + Assert.Equal(pagedResult.Value.ToArray(), new[] { 6, 7, 8, 9, 10 }); + + await cache.ListAddAsync("testpaging", new[] { 21, 22 }); + + pagedResult = await cache.GetListAsync("testpaging", 5, 5); + Assert.NotNull(pagedResult); + Assert.Equal(2, pagedResult.Value.Count); + Assert.Equal(pagedResult.Value.ToArray(), new[] { 21, 22 }); + + await cache.ListRemoveAsync("testpaging", 2); + pagedResult = await cache.GetListAsync("testpaging", 1, 5); + Assert.NotNull(pagedResult); + Assert.Equal(5, pagedResult.Value.Count); + Assert.Equal(pagedResult.Value.ToArray(), new[] { 1, 3, 4, 5, 6 }); } + } - public virtual async Task MeasureThroughputAsync() + public virtual async Task MeasureThroughputAsync() + { + var cache = GetCacheClient(); + if (cache == null) + return; + + using (cache) { - var cache = GetCacheClient(); - if (cache == null) - return; + await cache.RemoveAllAsync(); - using (cache) + var start = SystemClock.UtcNow; + const int itemCount = 10000; + var metrics = new InMemoryMetricsClient(new InMemoryMetricsClientOptions()); + for (int i = 0; i < itemCount; i++) { - await cache.RemoveAllAsync(); - - var start = SystemClock.UtcNow; - const int itemCount = 10000; - var metrics = new InMemoryMetricsClient(new InMemoryMetricsClientOptions()); - for (int i = 0; i < itemCount; i++) - { - await cache.SetAsync("test", 13422); - await cache.SetAsync("flag", true); - Assert.Equal(13422, (await cache.GetAsync("test")).Value); - Assert.Null(await cache.GetAsync("test2")); - Assert.True((await cache.GetAsync("flag")).Value); - metrics.Counter("work"); - } - - var workCounter = metrics.GetCounterStatsAsync("work", start, SystemClock.UtcNow); + await cache.SetAsync("test", 13422); + await cache.SetAsync("flag", true); + Assert.Equal(13422, (await cache.GetAsync("test")).Value); + Assert.Null(await cache.GetAsync("test2")); + Assert.True((await cache.GetAsync("flag")).Value); + metrics.Counter("work"); } + + var workCounter = metrics.GetCounterStatsAsync("work", start, SystemClock.UtcNow); } + } + + public virtual async Task MeasureSerializerSimpleThroughputAsync() + { + var cache = GetCacheClient(); + if (cache == null) + return; - public virtual async Task MeasureSerializerSimpleThroughputAsync() + using (cache) { - var cache = GetCacheClient(); - if (cache == null) - return; + await cache.RemoveAllAsync(); - using (cache) + var start = SystemClock.UtcNow; + const int itemCount = 10000; + var metrics = new InMemoryMetricsClient(new InMemoryMetricsClientOptions()); + for (int i = 0; i < itemCount; i++) { - await cache.RemoveAllAsync(); - - var start = SystemClock.UtcNow; - const int itemCount = 10000; - var metrics = new InMemoryMetricsClient(new InMemoryMetricsClientOptions()); - for (int i = 0; i < itemCount; i++) + await cache.SetAsync("test", new SimpleModel { - await cache.SetAsync("test", new SimpleModel - { - Data1 = "Hello", - Data2 = 12 - }); - var model = await cache.GetAsync("test"); - Assert.True(model.HasValue); - Assert.Equal("Hello", model.Value.Data1); - Assert.Equal(12, model.Value.Data2); - metrics.Counter("work"); - } - - var workCounter = metrics.GetCounterStatsAsync("work", start, SystemClock.UtcNow); + Data1 = "Hello", + Data2 = 12 + }); + var model = await cache.GetAsync("test"); + Assert.True(model.HasValue); + Assert.Equal("Hello", model.Value.Data1); + Assert.Equal(12, model.Value.Data2); + metrics.Counter("work"); } + + var workCounter = metrics.GetCounterStatsAsync("work", start, SystemClock.UtcNow); } + } - public virtual async Task MeasureSerializerComplexThroughputAsync() + public virtual async Task MeasureSerializerComplexThroughputAsync() + { + var cache = GetCacheClient(); + if (cache == null) + return; + + using (cache) { - var cache = GetCacheClient(); - if (cache == null) - return; + await cache.RemoveAllAsync(); - using (cache) + var start = SystemClock.UtcNow; + const int itemCount = 10000; + var metrics = new InMemoryMetricsClient(new InMemoryMetricsClientOptions()); + for (int i = 0; i < itemCount; i++) { - await cache.RemoveAllAsync(); - - var start = SystemClock.UtcNow; - const int itemCount = 10000; - var metrics = new InMemoryMetricsClient(new InMemoryMetricsClientOptions()); - for (int i = 0; i < itemCount; i++) + await cache.SetAsync("test", new ComplexModel { - await cache.SetAsync("test", new ComplexModel + Data1 = "Hello", + Data2 = 12, + Data3 = true, + Simple = new SimpleModel { - Data1 = "Hello", - Data2 = 12, - Data3 = true, - Simple = new SimpleModel - { - Data1 = "hi", - Data2 = 13 - }, - Simples = new List { - new SimpleModel { - Data1 = "hey", - Data2 = 45 - }, - new SimpleModel { - Data1 = "next", - Data2 = 3423 - } + Data1 = "hi", + Data2 = 13 + }, + Simples = new List { + new SimpleModel { + Data1 = "hey", + Data2 = 45 }, - DictionarySimples = new Dictionary { - { "sdf", new SimpleModel { Data1 = "Sachin" } } - }, - - DerivedDictionarySimples = new SampleDictionary { - { "sdf", new SimpleModel { Data1 = "Sachin" } } + new SimpleModel { + Data1 = "next", + Data2 = 3423 } - }); - - var model = await cache.GetAsync("test"); - Assert.True(model.HasValue); - Assert.Equal("Hello", model.Value.Data1); - Assert.Equal(12, model.Value.Data2); - metrics.Counter("work"); - } + }, + DictionarySimples = new Dictionary { + { "sdf", new SimpleModel { Data1 = "Sachin" } } + }, + + DerivedDictionarySimples = new SampleDictionary { + { "sdf", new SimpleModel { Data1 = "Sachin" } } + } + }); - var workCounter = metrics.GetCounterStatsAsync("work", start, SystemClock.UtcNow); + var model = await cache.GetAsync("test"); + Assert.True(model.HasValue); + Assert.Equal("Hello", model.Value.Data1); + Assert.Equal(12, model.Value.Data2); + metrics.Counter("work"); } + + var workCounter = metrics.GetCounterStatsAsync("work", start, SystemClock.UtcNow); } } +} + +public class SimpleModel +{ + public string Data1 { get; set; } + public int Data2 { get; set; } +} + +public class ComplexModel +{ + public string Data1 { get; set; } + public int Data2 { get; set; } + public SimpleModel Simple { get; set; } + public ICollection Simples { get; set; } + public bool Data3 { get; set; } + public IDictionary DictionarySimples { get; set; } + public SampleDictionary DerivedDictionarySimples { get; set; } +} + +public class MyData +{ + private readonly string _blah = "blah"; + public string Blah => _blah; + public string Type { get; set; } + public DateTimeOffset Date { get; set; } + public string Message { get; set; } +} - public class SimpleModel +public class SampleDictionary : IDictionary +{ + private readonly IDictionary _dictionary; + + public SampleDictionary() { - public string Data1 { get; set; } - public int Data2 { get; set; } + _dictionary = new Dictionary(); } - public class ComplexModel + public SampleDictionary(IDictionary dictionary) { - public string Data1 { get; set; } - public int Data2 { get; set; } - public SimpleModel Simple { get; set; } - public ICollection Simples { get; set; } - public bool Data3 { get; set; } - public IDictionary DictionarySimples { get; set; } - public SampleDictionary DerivedDictionarySimples { get; set; } + _dictionary = new Dictionary(dictionary); } - public class MyData + public SampleDictionary(IEqualityComparer comparer) { - private readonly string _blah = "blah"; - public string Blah => _blah; - public string Type { get; set; } - public DateTimeOffset Date { get; set; } - public string Message { get; set; } + _dictionary = new Dictionary(comparer); } - public class SampleDictionary : IDictionary + public SampleDictionary(IDictionary dictionary, IEqualityComparer comparer) { - private readonly IDictionary _dictionary; - - public SampleDictionary() - { - _dictionary = new Dictionary(); - } - - public SampleDictionary(IDictionary dictionary) - { - _dictionary = new Dictionary(dictionary); - } - - public SampleDictionary(IEqualityComparer comparer) - { - _dictionary = new Dictionary(comparer); - } - - public SampleDictionary(IDictionary dictionary, IEqualityComparer comparer) - { - _dictionary = new Dictionary(dictionary, comparer); - } + _dictionary = new Dictionary(dictionary, comparer); + } - public void Add(TKey key, TValue value) - { - _dictionary.Add(key, value); - } + public void Add(TKey key, TValue value) + { + _dictionary.Add(key, value); + } - public void Add(KeyValuePair item) - { - _dictionary.Add(item); - } + public void Add(KeyValuePair item) + { + _dictionary.Add(item); + } - public bool Remove(TKey key) - { - return _dictionary.Remove(key); - } + public bool Remove(TKey key) + { + return _dictionary.Remove(key); + } - public bool Remove(KeyValuePair item) - { - return _dictionary.Remove(item); - } + public bool Remove(KeyValuePair item) + { + return _dictionary.Remove(item); + } - public void Clear() - { - _dictionary.Clear(); - } + public void Clear() + { + _dictionary.Clear(); + } - public bool ContainsKey(TKey key) - { - return _dictionary.ContainsKey(key); - } + public bool ContainsKey(TKey key) + { + return _dictionary.ContainsKey(key); + } - public bool Contains(KeyValuePair item) - { - return _dictionary.Contains(item); - } + public bool Contains(KeyValuePair item) + { + return _dictionary.Contains(item); + } - public bool TryGetValue(TKey key, out TValue value) - { - return _dictionary.TryGetValue(key, out value); - } + public bool TryGetValue(TKey key, out TValue value) + { + return _dictionary.TryGetValue(key, out value); + } - public void CopyTo(KeyValuePair[] array, int arrayIndex) - { - _dictionary.CopyTo(array, arrayIndex); - } + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + _dictionary.CopyTo(array, arrayIndex); + } - public ICollection Keys => _dictionary.Keys; + public ICollection Keys => _dictionary.Keys; - public ICollection Values => _dictionary.Values; + public ICollection Values => _dictionary.Values; - public int Count => _dictionary.Count; + public int Count => _dictionary.Count; - public bool IsReadOnly => _dictionary.IsReadOnly; + public bool IsReadOnly => _dictionary.IsReadOnly; - public TValue this[TKey key] - { - get => _dictionary[key]; - set => _dictionary[key] = value; - } + public TValue this[TKey key] + { + get => _dictionary[key]; + set => _dictionary[key] = value; + } - public IEnumerator> GetEnumerator() - { - return _dictionary.GetEnumerator(); - } + public IEnumerator> GetEnumerator() + { + return _dictionary.GetEnumerator(); + } - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); } } diff --git a/src/Foundatio.TestHarness/Caching/HybridCacheClientTests.cs b/src/Foundatio.TestHarness/Caching/HybridCacheClientTests.cs index 330cb7528..4382a207f 100644 --- a/src/Foundatio.TestHarness/Caching/HybridCacheClientTests.cs +++ b/src/Foundatio.TestHarness/Caching/HybridCacheClientTests.cs @@ -9,231 +9,230 @@ using Xunit; using Xunit.Abstractions; -namespace Foundatio.Tests.Caching +namespace Foundatio.Tests.Caching; + +public class HybridCacheClientTests : CacheClientTestsBase, IDisposable { - public class HybridCacheClientTests : CacheClientTestsBase, IDisposable + protected readonly ICacheClient _distributedCache = new InMemoryCacheClient(new InMemoryCacheClientOptions()); + protected readonly IMessageBus _messageBus = new InMemoryMessageBus(new InMemoryMessageBusOptions()); + + public HybridCacheClientTests(ITestOutputHelper output) : base(output) { } + + protected override ICacheClient GetCacheClient(bool shouldThrowOnSerializationError = true) { - protected readonly ICacheClient _distributedCache = new InMemoryCacheClient(new InMemoryCacheClientOptions()); - protected readonly IMessageBus _messageBus = new InMemoryMessageBus(new InMemoryMessageBusOptions()); + return new HybridCacheClient(_distributedCache, _messageBus, new InMemoryCacheClientOptions { CloneValues = true, ShouldThrowOnSerializationError = shouldThrowOnSerializationError }, Log); + } - public HybridCacheClientTests(ITestOutputHelper output) : base(output) { } + [Fact] + public override Task CanGetAllAsync() + { + return base.CanGetAllAsync(); + } - protected override ICacheClient GetCacheClient(bool shouldThrowOnSerializationError = true) - { - return new HybridCacheClient(_distributedCache, _messageBus, new InMemoryCacheClientOptions { CloneValues = true, ShouldThrowOnSerializationError = shouldThrowOnSerializationError }, Log); - } + [Fact] + public override Task CanGetAllWithOverlapAsync() + { + return base.CanGetAllWithOverlapAsync(); + } - [Fact] - public override Task CanGetAllAsync() - { - return base.CanGetAllAsync(); - } + [Fact] + public override Task CanSetAsync() + { + return base.CanSetAsync(); + } - [Fact] - public override Task CanGetAllWithOverlapAsync() - { - return base.CanGetAllWithOverlapAsync(); - } + [Fact] + public override Task CanSetAndGetValueAsync() + { + return base.CanSetAndGetValueAsync(); + } - [Fact] - public override Task CanSetAsync() - { - return base.CanSetAsync(); - } + [Fact] + public override Task CanAddAsync() + { + return base.CanAddAsync(); + } - [Fact] - public override Task CanSetAndGetValueAsync() - { - return base.CanSetAndGetValueAsync(); - } + [Fact] + public override Task CanAddConcurrentlyAsync() + { + return base.CanAddConcurrentlyAsync(); + } - [Fact] - public override Task CanAddAsync() - { - return base.CanAddAsync(); - } + [Fact] + public override Task CanTryGetAsync() + { + return base.CanTryGetAsync(); + } - [Fact] - public override Task CanAddConcurrentlyAsync() - { - return base.CanAddConcurrentlyAsync(); - } + [Fact] + public override Task CanUseScopedCachesAsync() + { + return base.CanUseScopedCachesAsync(); + } - [Fact] - public override Task CanTryGetAsync() - { - return base.CanTryGetAsync(); - } + [Fact] + public override Task CanSetAndGetObjectAsync() + { + return base.CanSetAndGetObjectAsync(); + } - [Fact] - public override Task CanUseScopedCachesAsync() - { - return base.CanUseScopedCachesAsync(); - } + [Fact] + public override Task CanRemoveByPrefixAsync() + { + return base.CanRemoveByPrefixAsync(); + } - [Fact] - public override Task CanSetAndGetObjectAsync() - { - return base.CanSetAndGetObjectAsync(); - } + [Theory] + [InlineData(50)] + [InlineData(500)] + public override Task CanRemoveByPrefixMultipleEntriesAsync(int count) + { + return base.CanRemoveByPrefixMultipleEntriesAsync(count); + } - [Fact] - public override Task CanRemoveByPrefixAsync() - { - return base.CanRemoveByPrefixAsync(); - } + [Fact] + public override Task CanSetExpirationAsync() + { + return base.CanSetExpirationAsync(); + } - [Theory] - [InlineData(50)] - [InlineData(500)] - public override Task CanRemoveByPrefixMultipleEntriesAsync(int count) - { - return base.CanRemoveByPrefixMultipleEntriesAsync(count); - } + [Fact] + public override Task CanIncrementAsync() + { + return base.CanIncrementAsync(); + } - [Fact] - public override Task CanSetExpirationAsync() - { - return base.CanSetExpirationAsync(); - } + [Fact] + public override Task CanIncrementAndExpireAsync() + { + return base.CanIncrementAndExpireAsync(); + } - [Fact] - public override Task CanIncrementAsync() - { - return base.CanIncrementAsync(); - } + [Fact] + public override Task CanGetAndSetDateTimeAsync() + { + return base.CanGetAndSetDateTimeAsync(); + } - [Fact] - public override Task CanIncrementAndExpireAsync() - { - return base.CanIncrementAndExpireAsync(); - } + [Fact] + public override Task CanRoundTripLargeNumbersAsync() + { + return base.CanRoundTripLargeNumbersAsync(); + } - [Fact] - public override Task CanGetAndSetDateTimeAsync() - { - return base.CanGetAndSetDateTimeAsync(); - } + [Fact] + public override Task CanRoundTripLargeNumbersWithExpirationAsync() + { + return base.CanRoundTripLargeNumbersWithExpirationAsync(); + } - [Fact] - public override Task CanRoundTripLargeNumbersAsync() - { - return base.CanRoundTripLargeNumbersAsync(); - } + [Fact] + public override Task CanManageListsAsync() + { + return base.CanManageListsAsync(); + } - [Fact] - public override Task CanRoundTripLargeNumbersWithExpirationAsync() - { - return base.CanRoundTripLargeNumbersWithExpirationAsync(); - } + [Fact] + public virtual async Task WillUseLocalCache() + { + using var firstCache = GetCacheClient() as HybridCacheClient; + Assert.NotNull(firstCache); - [Fact] - public override Task CanManageListsAsync() - { - return base.CanManageListsAsync(); - } + using var secondCache = GetCacheClient() as HybridCacheClient; + Assert.NotNull(secondCache); - [Fact] - public virtual async Task WillUseLocalCache() - { - using var firstCache = GetCacheClient() as HybridCacheClient; - Assert.NotNull(firstCache); + await firstCache.SetAsync("first1", 1); + await firstCache.IncrementAsync("first2"); + Assert.Equal(1, firstCache.LocalCache.Count); - using var secondCache = GetCacheClient() as HybridCacheClient; - Assert.NotNull(secondCache); + string cacheKey = Guid.NewGuid().ToString("N").Substring(10); + await firstCache.SetAsync(cacheKey, new SimpleModel { Data1 = "test" }); + Assert.Equal(2, firstCache.LocalCache.Count); + Assert.Equal(0, secondCache.LocalCache.Count); + Assert.Equal(0, firstCache.LocalCacheHits); - await firstCache.SetAsync("first1", 1); - await firstCache.IncrementAsync("first2"); - Assert.Equal(1, firstCache.LocalCache.Count); + Assert.True((await firstCache.GetAsync(cacheKey)).HasValue); + Assert.Equal(1, firstCache.LocalCacheHits); + Assert.Equal(2, firstCache.LocalCache.Count); - string cacheKey = Guid.NewGuid().ToString("N").Substring(10); - await firstCache.SetAsync(cacheKey, new SimpleModel { Data1 = "test" }); - Assert.Equal(2, firstCache.LocalCache.Count); - Assert.Equal(0, secondCache.LocalCache.Count); - Assert.Equal(0, firstCache.LocalCacheHits); + Assert.True((await secondCache.GetAsync(cacheKey)).HasValue); + Assert.Equal(0, secondCache.LocalCacheHits); + Assert.Equal(1, secondCache.LocalCache.Count); - Assert.True((await firstCache.GetAsync(cacheKey)).HasValue); - Assert.Equal(1, firstCache.LocalCacheHits); - Assert.Equal(2, firstCache.LocalCache.Count); + Assert.True((await secondCache.GetAsync(cacheKey)).HasValue); + Assert.Equal(1, secondCache.LocalCacheHits); + } - Assert.True((await secondCache.GetAsync(cacheKey)).HasValue); - Assert.Equal(0, secondCache.LocalCacheHits); - Assert.Equal(1, secondCache.LocalCache.Count); + [Fact] + public virtual async Task WillExpireRemoteItems() + { + using var firstCache = GetCacheClient() as HybridCacheClient; + Assert.NotNull(firstCache); + var firstResetEvent = new AsyncAutoResetEvent(false); - Assert.True((await secondCache.GetAsync(cacheKey)).HasValue); - Assert.Equal(1, secondCache.LocalCacheHits); + void ExpiredHandler(object sender, ItemExpiredEventArgs args) + { + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.LogTrace("First local cache expired: {Key}", args.Key); + firstResetEvent.Set(); } - [Fact] - public virtual async Task WillExpireRemoteItems() + using (firstCache.LocalCache.ItemExpired.AddSyncHandler(ExpiredHandler)) { - using var firstCache = GetCacheClient() as HybridCacheClient; - Assert.NotNull(firstCache); - var firstResetEvent = new AsyncAutoResetEvent(false); + using var secondCache = GetCacheClient() as HybridCacheClient; + Assert.NotNull(secondCache); + var secondResetEvent = new AsyncAutoResetEvent(false); - void ExpiredHandler(object sender, ItemExpiredEventArgs args) + void ExpiredHandler2(object sender, ItemExpiredEventArgs args) { if (_logger.IsEnabled(LogLevel.Trace)) - _logger.LogTrace("First local cache expired: {Key}", args.Key); - firstResetEvent.Set(); + _logger.LogTrace("Second local cache expired: {Key}", args.Key); + secondResetEvent.Set(); } - using (firstCache.LocalCache.ItemExpired.AddSyncHandler(ExpiredHandler)) + using (secondCache.LocalCache.ItemExpired.AddSyncHandler(ExpiredHandler2)) { - using var secondCache = GetCacheClient() as HybridCacheClient; - Assert.NotNull(secondCache); - var secondResetEvent = new AsyncAutoResetEvent(false); - - void ExpiredHandler2(object sender, ItemExpiredEventArgs args) - { - if (_logger.IsEnabled(LogLevel.Trace)) - _logger.LogTrace("Second local cache expired: {Key}", args.Key); - secondResetEvent.Set(); - } - - using (secondCache.LocalCache.ItemExpired.AddSyncHandler(ExpiredHandler2)) - { - string cacheKey = "will-expire-remote"; - _logger.LogTrace("First Set"); - Assert.True(await firstCache.AddAsync(cacheKey, new SimpleModel { Data1 = "test" }, TimeSpan.FromMilliseconds(250))); - _logger.LogTrace("Done First Set"); - Assert.Equal(1, firstCache.LocalCache.Count); - - _logger.LogTrace("Second Get"); - Assert.True((await secondCache.GetAsync(cacheKey)).HasValue); - _logger.LogTrace("Done Second Get"); - Assert.Equal(1, secondCache.LocalCache.Count); - - _logger.LogTrace("Waiting for item expired handlers..."); - var sw = Stopwatch.StartNew(); - await firstResetEvent.WaitAsync(TimeSpan.FromSeconds(2)); - await secondResetEvent.WaitAsync(TimeSpan.FromSeconds(2)); - sw.Stop(); - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Time {Elapsed:g}", sw.Elapsed); - } + string cacheKey = "will-expire-remote"; + _logger.LogTrace("First Set"); + Assert.True(await firstCache.AddAsync(cacheKey, new SimpleModel { Data1 = "test" }, TimeSpan.FromMilliseconds(250))); + _logger.LogTrace("Done First Set"); + Assert.Equal(1, firstCache.LocalCache.Count); + + _logger.LogTrace("Second Get"); + Assert.True((await secondCache.GetAsync(cacheKey)).HasValue); + _logger.LogTrace("Done Second Get"); + Assert.Equal(1, secondCache.LocalCache.Count); + + _logger.LogTrace("Waiting for item expired handlers..."); + var sw = Stopwatch.StartNew(); + await firstResetEvent.WaitAsync(TimeSpan.FromSeconds(2)); + await secondResetEvent.WaitAsync(TimeSpan.FromSeconds(2)); + sw.Stop(); + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Time {Elapsed:g}", sw.Elapsed); } } + } - [Fact] - public virtual async Task WillWorkWithSets() - { - using var firstCache = GetCacheClient() as HybridCacheClient; - Assert.NotNull(firstCache); + [Fact] + public virtual async Task WillWorkWithSets() + { + using var firstCache = GetCacheClient() as HybridCacheClient; + Assert.NotNull(firstCache); - using var secondCache = GetCacheClient() as HybridCacheClient; - Assert.NotNull(secondCache); + using var secondCache = GetCacheClient() as HybridCacheClient; + Assert.NotNull(secondCache); - await firstCache.ListAddAsync("set1", new[] { 1, 2, 3 }); + await firstCache.ListAddAsync("set1", new[] { 1, 2, 3 }); - var values = await secondCache.GetListAsync("set1"); + var values = await secondCache.GetListAsync("set1"); - Assert.Equal(3, values.Value.Count); - } + Assert.Equal(3, values.Value.Count); + } - public void Dispose() - { - _distributedCache.Dispose(); - _messageBus.Dispose(); - } + public void Dispose() + { + _distributedCache.Dispose(); + _messageBus.Dispose(); } } diff --git a/src/Foundatio.TestHarness/Extensions/TaskExtensions.cs b/src/Foundatio.TestHarness/Extensions/TaskExtensions.cs index e5c9a2def..eeaef9334 100644 --- a/src/Foundatio.TestHarness/Extensions/TaskExtensions.cs +++ b/src/Foundatio.TestHarness/Extensions/TaskExtensions.cs @@ -4,27 +4,26 @@ using Foundatio.AsyncEx; using Foundatio.Utility; -namespace Foundatio.Tests.Extensions +namespace Foundatio.Tests.Extensions; + +public static class TaskExtensions { - public static class TaskExtensions + [DebuggerStepThrough] + public static async Task WaitAsync(this AsyncManualResetEvent resetEvent, TimeSpan timeout) { - [DebuggerStepThrough] - public static async Task WaitAsync(this AsyncManualResetEvent resetEvent, TimeSpan timeout) - { - using var timeoutCancellationTokenSource = timeout.ToCancellationTokenSource(); - await resetEvent.WaitAsync(timeoutCancellationTokenSource.Token).AnyContext(); - } + using var timeoutCancellationTokenSource = timeout.ToCancellationTokenSource(); + await resetEvent.WaitAsync(timeoutCancellationTokenSource.Token).AnyContext(); + } - [DebuggerStepThrough] - public static async Task WaitAsync(this AsyncAutoResetEvent resetEvent, TimeSpan timeout) - { - using var timeoutCancellationTokenSource = timeout.ToCancellationTokenSource(); - await resetEvent.WaitAsync(timeoutCancellationTokenSource.Token).AnyContext(); - } + [DebuggerStepThrough] + public static async Task WaitAsync(this AsyncAutoResetEvent resetEvent, TimeSpan timeout) + { + using var timeoutCancellationTokenSource = timeout.ToCancellationTokenSource(); + await resetEvent.WaitAsync(timeoutCancellationTokenSource.Token).AnyContext(); + } - public static Task WaitAsync(this AsyncCountdownEvent countdownEvent, TimeSpan timeout) - { - return Task.WhenAny(countdownEvent.WaitAsync(), SystemClock.SleepAsync(timeout)); - } + public static Task WaitAsync(this AsyncCountdownEvent countdownEvent, TimeSpan timeout) + { + return Task.WhenAny(countdownEvent.WaitAsync(), SystemClock.SleepAsync(timeout)); } } diff --git a/src/Foundatio.TestHarness/Jobs/HelloWorldJob.cs b/src/Foundatio.TestHarness/Jobs/HelloWorldJob.cs index 02bef7d44..429ad68e7 100644 --- a/src/Foundatio.TestHarness/Jobs/HelloWorldJob.cs +++ b/src/Foundatio.TestHarness/Jobs/HelloWorldJob.cs @@ -4,79 +4,78 @@ using Foundatio.Jobs; using Microsoft.Extensions.Logging; -namespace Foundatio.Tests.Jobs +namespace Foundatio.Tests.Jobs; + +public class HelloWorldJob : JobBase { - public class HelloWorldJob : JobBase - { - private readonly string _id; + private readonly string _id; - public HelloWorldJob(ILoggerFactory loggerFactory) : base(loggerFactory) - { - _id = Guid.NewGuid().ToString("N").Substring(0, 10); - } + public HelloWorldJob(ILoggerFactory loggerFactory) : base(loggerFactory) + { + _id = Guid.NewGuid().ToString("N").Substring(0, 10); + } - public static int GlobalRunCount; - public int RunCount { get; set; } + public static int GlobalRunCount; + public int RunCount { get; set; } - protected override Task RunInternalAsync(JobContext context) - { - RunCount++; - Interlocked.Increment(ref GlobalRunCount); + protected override Task RunInternalAsync(JobContext context) + { + RunCount++; + Interlocked.Increment(ref GlobalRunCount); - if (_logger.IsEnabled(LogLevel.Trace)) - _logger.LogTrace("HelloWorld Running: instance={Id} runs={RunCount} global={GlobalRunCount}", _id, RunCount, GlobalRunCount); + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.LogTrace("HelloWorld Running: instance={Id} runs={RunCount} global={GlobalRunCount}", _id, RunCount, GlobalRunCount); - return Task.FromResult(JobResult.Success); - } + return Task.FromResult(JobResult.Success); } +} - public class FailingJob : JobBase - { - private readonly string _id; +public class FailingJob : JobBase +{ + private readonly string _id; - public int RunCount { get; set; } + public int RunCount { get; set; } - public FailingJob(ILoggerFactory loggerFactory) : base(loggerFactory) - { - _id = Guid.NewGuid().ToString("N").Substring(0, 10); - } + public FailingJob(ILoggerFactory loggerFactory) : base(loggerFactory) + { + _id = Guid.NewGuid().ToString("N").Substring(0, 10); + } - protected override Task RunInternalAsync(JobContext context) - { - RunCount++; + protected override Task RunInternalAsync(JobContext context) + { + RunCount++; - if (_logger.IsEnabled(LogLevel.Trace)) - _logger.LogTrace("FailingJob Running: instance={Id} runs={RunCount}", _id, RunCount); + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.LogTrace("FailingJob Running: instance={Id} runs={RunCount}", _id, RunCount); - return Task.FromResult(JobResult.FailedWithMessage("Test failure")); - } + return Task.FromResult(JobResult.FailedWithMessage("Test failure")); } +} - public class LongRunningJob : JobBase +public class LongRunningJob : JobBase +{ + private readonly string _id; + private int _iterationCount; + + public LongRunningJob(ILoggerFactory loggerFactory) : base(loggerFactory) { - private readonly string _id; - private int _iterationCount; + _id = Guid.NewGuid().ToString("N").Substring(0, 10); + } + + public int IterationCount => _iterationCount; - public LongRunningJob(ILoggerFactory loggerFactory) : base(loggerFactory) + protected override Task RunInternalAsync(JobContext context) + { + do { - _id = Guid.NewGuid().ToString("N").Substring(0, 10); - } + Interlocked.Increment(ref _iterationCount); + if (context.CancellationToken.IsCancellationRequested) + break; - public int IterationCount => _iterationCount; + if (_iterationCount % 10000 == 0 && _logger.IsEnabled(LogLevel.Trace)) + _logger.LogTrace("LongRunningJob Running: instance={Id} iterations={IterationCount}", _id, IterationCount); + } while (true); - protected override Task RunInternalAsync(JobContext context) - { - do - { - Interlocked.Increment(ref _iterationCount); - if (context.CancellationToken.IsCancellationRequested) - break; - - if (_iterationCount % 10000 == 0 && _logger.IsEnabled(LogLevel.Trace)) - _logger.LogTrace("LongRunningJob Running: instance={Id} iterations={IterationCount}", _id, IterationCount); - } while (true); - - return Task.FromResult(JobResult.Success); - } + return Task.FromResult(JobResult.Success); } } diff --git a/src/Foundatio.TestHarness/Jobs/JobQueueTestsBase.cs b/src/Foundatio.TestHarness/Jobs/JobQueueTestsBase.cs index 2ea57daaf..8af588f61 100644 --- a/src/Foundatio.TestHarness/Jobs/JobQueueTestsBase.cs +++ b/src/Foundatio.TestHarness/Jobs/JobQueueTestsBase.cs @@ -16,183 +16,181 @@ using Xunit; using Xunit.Abstractions; -namespace Foundatio.Tests.Jobs +namespace Foundatio.Tests.Jobs; + +public abstract class JobQueueTestsBase : TestWithLoggingBase { - public abstract class JobQueueTestsBase : TestWithLoggingBase - { - private readonly ActivitySource _activitySource = new(nameof(JobQueueTestsBase)); + private readonly ActivitySource _activitySource = new(nameof(JobQueueTestsBase)); - public JobQueueTestsBase(ITestOutputHelper output) : base(output) { } + public JobQueueTestsBase(ITestOutputHelper output) : base(output) { } - protected abstract IQueue GetSampleWorkItemQueue(int retries, TimeSpan retryDelay); + protected abstract IQueue GetSampleWorkItemQueue(int retries, TimeSpan retryDelay); - public virtual async Task ActivityWillFlowThroughQueueJobAsync() - { - using var queue = GetSampleWorkItemQueue(retries: 0, retryDelay: TimeSpan.Zero); - await queue.DeleteQueueAsync(); + public virtual async Task ActivityWillFlowThroughQueueJobAsync() + { + using var queue = GetSampleWorkItemQueue(retries: 0, retryDelay: TimeSpan.Zero); + await queue.DeleteQueueAsync(); - Activity parentActivity = null; - using var listener = new ActivityListener + Activity parentActivity = null; + using var listener = new ActivityListener + { + ShouldListenTo = s => s.Name == nameof(JobQueueTestsBase) || s.Name == "Foundatio", + Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded, + ActivityStarted = a => { - ShouldListenTo = s => s.Name == nameof(JobQueueTestsBase) || s.Name == "Foundatio", - Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded, - ActivityStarted = a => - { - if (a.OperationName != "ProcessQueueEntry") - return; + if (a.OperationName != "ProcessQueueEntry") + return; - Assert.Equal(parentActivity.RootId, a.RootId); - Assert.Equal(parentActivity.SpanId, a.ParentSpanId); - }, - ActivityStopped = a => { } - }; - ActivitySource.AddActivityListener(listener); + Assert.Equal(parentActivity.RootId, a.RootId); + Assert.Equal(parentActivity.SpanId, a.ParentSpanId); + }, + ActivityStopped = a => { } + }; + ActivitySource.AddActivityListener(listener); - parentActivity = _activitySource.StartActivity("Parent"); - Assert.NotNull(parentActivity); + parentActivity = _activitySource.StartActivity("Parent"); + Assert.NotNull(parentActivity); - var enqueueTask = await queue.EnqueueAsync(new SampleQueueWorkItem - { - Created = SystemClock.UtcNow, - Path = "somepath" - }); + var enqueueTask = await queue.EnqueueAsync(new SampleQueueWorkItem + { + Created = SystemClock.UtcNow, + Path = "somepath" + }); - // clear activity and then verify that - Activity.Current = null; + // clear activity and then verify that Activity.Current = null; - var job = new SampleQueueJob(queue, null, Log); - await job.RunAsync(); + var job = new SampleQueueJob(queue, null, Log); + await job.RunAsync(); - var stats = await queue.GetQueueStatsAsync(); - Assert.Equal(0, stats.Queued); - Assert.Equal(1, stats.Enqueued); - Assert.Equal(1, stats.Dequeued); - } + var stats = await queue.GetQueueStatsAsync(); + Assert.Equal(0, stats.Queued); + Assert.Equal(1, stats.Enqueued); + Assert.Equal(1, stats.Dequeued); + } + + public virtual async Task CanRunQueueJobAsync() + { + const int workItemCount = 100; + using var queue = GetSampleWorkItemQueue(retries: 0, retryDelay: TimeSpan.Zero); + await queue.DeleteQueueAsync(); - public virtual async Task CanRunQueueJobAsync() + var enqueueTask = Run.InParallelAsync(workItemCount, index => queue.EnqueueAsync(new SampleQueueWorkItem { - const int workItemCount = 100; - using var queue = GetSampleWorkItemQueue(retries: 0, retryDelay: TimeSpan.Zero); - await queue.DeleteQueueAsync(); + Created = SystemClock.UtcNow, + Path = "somepath" + index + })); + + var job = new SampleQueueJob(queue, null, Log); + await SystemClock.SleepAsync(10); + await Task.WhenAll(job.RunUntilEmptyAsync(), enqueueTask); + + var stats = await queue.GetQueueStatsAsync(); + Assert.Equal(0, stats.Queued); + Assert.Equal(workItemCount, stats.Enqueued); + Assert.Equal(workItemCount, stats.Dequeued); + } + + public virtual async Task CanRunQueueJobWithLockFailAsync() + { + const int workItemCount = 10; + const int allowedLockCount = 5; + Log.SetLogLevel(LogLevel.Trace); - var enqueueTask = Run.InParallelAsync(workItemCount, index => queue.EnqueueAsync(new SampleQueueWorkItem + using var queue = GetSampleWorkItemQueue(retries: 3, retryDelay: TimeSpan.Zero); + await queue.DeleteQueueAsync(); + + var enqueueTask = Run.InParallelAsync(workItemCount, index => + { + _logger.LogInformation($"Enqueue #{index}"); + return queue.EnqueueAsync(new SampleQueueWorkItem { Created = SystemClock.UtcNow, Path = "somepath" + index - })); + }); + }); + + var lockProvider = new ThrottlingLockProvider(new InMemoryCacheClient(new InMemoryCacheClientOptions()), allowedLockCount, TimeSpan.FromDays(1), Log); + var job = new SampleQueueJobWithLocking(queue, null, lockProvider, Log); + await SystemClock.SleepAsync(10); + _logger.LogInformation("Starting RunUntilEmptyAsync"); + await Task.WhenAll(job.RunUntilEmptyAsync(), enqueueTask); + _logger.LogInformation("Done RunUntilEmptyAsync"); + + var stats = await queue.GetQueueStatsAsync(); + Assert.Equal(0, stats.Queued); + Assert.Equal(workItemCount, stats.Enqueued); + Assert.Equal(allowedLockCount, stats.Completed); + Assert.Equal(allowedLockCount * 4, stats.Abandoned); + Assert.Equal(allowedLockCount, stats.Deadletter); + } - var job = new SampleQueueJob(queue, null, Log); - await SystemClock.SleepAsync(10); - await Task.WhenAll(job.RunUntilEmptyAsync(), enqueueTask); + public virtual async Task CanRunMultipleQueueJobsAsync() + { + const int jobCount = 5; + const int workItemCount = 100; - var stats = await queue.GetQueueStatsAsync(); - Assert.Equal(0, stats.Queued); - Assert.Equal(workItemCount, stats.Enqueued); - Assert.Equal(workItemCount, stats.Dequeued); - } + Log.SetLogLevel(LogLevel.Information); + Log.SetLogLevel(LogLevel.None); - public virtual async Task CanRunQueueJobWithLockFailAsync() + using var metrics = new InMemoryMetricsClient(new InMemoryMetricsClientOptions { LoggerFactory = Log, Buffered = true }); + var queues = new List>(); + try { - const int workItemCount = 10; - const int allowedLockCount = 5; - Log.SetLogLevel(LogLevel.Trace); - - using var queue = GetSampleWorkItemQueue(retries: 3, retryDelay: TimeSpan.Zero); - await queue.DeleteQueueAsync(); + for (int i = 0; i < jobCount; i++) + { + var q = GetSampleWorkItemQueue(retries: 1, retryDelay: TimeSpan.Zero); + await q.DeleteQueueAsync(); + q.AttachBehavior(new MetricsQueueBehavior(metrics, "test", loggerFactory: Log)); + queues.Add(q); + } + _logger.LogInformation("Done setting up queues"); var enqueueTask = Run.InParallelAsync(workItemCount, index => { - _logger.LogInformation($"Enqueue #{index}"); + var queue = queues[RandomData.GetInt(0, jobCount - 1)]; return queue.EnqueueAsync(new SampleQueueWorkItem { Created = SystemClock.UtcNow, - Path = "somepath" + index + Path = RandomData.GetString() }); }); + _logger.LogInformation("Done enqueueing"); - var lockProvider = new ThrottlingLockProvider(new InMemoryCacheClient(new InMemoryCacheClientOptions()), allowedLockCount, TimeSpan.FromDays(1), Log); - var job = new SampleQueueJobWithLocking(queue, null, lockProvider, Log); - await SystemClock.SleepAsync(10); - _logger.LogInformation("Starting RunUntilEmptyAsync"); - await Task.WhenAll(job.RunUntilEmptyAsync(), enqueueTask); - _logger.LogInformation("Done RunUntilEmptyAsync"); - - var stats = await queue.GetQueueStatsAsync(); - Assert.Equal(0, stats.Queued); - Assert.Equal(workItemCount, stats.Enqueued); - Assert.Equal(allowedLockCount, stats.Completed); - Assert.Equal(allowedLockCount * 4, stats.Abandoned); - Assert.Equal(allowedLockCount, stats.Deadletter); - } - - public virtual async Task CanRunMultipleQueueJobsAsync() - { - const int jobCount = 5; - const int workItemCount = 100; + var cancellationTokenSource = new CancellationTokenSource(); + await Run.InParallelAsync(jobCount, async index => + { + var queue = queues[index - 1]; + var job = new SampleQueueWithRandomErrorsAndAbandonsJob(queue, metrics, Log); + await job.RunUntilEmptyAsync(cancellationTokenSource.Token); + await cancellationTokenSource.CancelAsync(); + }); + _logger.LogInformation("Done running jobs until empty"); - Log.SetLogLevel(LogLevel.Information); - Log.SetLogLevel(LogLevel.None); + await enqueueTask; - using var metrics = new InMemoryMetricsClient(new InMemoryMetricsClientOptions { LoggerFactory = Log, Buffered = true }); - var queues = new List>(); - try + var queueStats = new List(); + for (int i = 0; i < queues.Count; i++) { - for (int i = 0; i < jobCount; i++) - { - var q = GetSampleWorkItemQueue(retries: 1, retryDelay: TimeSpan.Zero); - await q.DeleteQueueAsync(); - q.AttachBehavior(new MetricsQueueBehavior(metrics, "test", loggerFactory: Log)); - queues.Add(q); - } - _logger.LogInformation("Done setting up queues"); - - var enqueueTask = Run.InParallelAsync(workItemCount, index => - { - var queue = queues[RandomData.GetInt(0, jobCount - 1)]; - return queue.EnqueueAsync(new SampleQueueWorkItem - { - Created = SystemClock.UtcNow, - Path = RandomData.GetString() - }); - }); - _logger.LogInformation("Done enqueueing"); - - var cancellationTokenSource = new CancellationTokenSource(); - await Run.InParallelAsync(jobCount, async index => - { - var queue = queues[index - 1]; - var job = new SampleQueueWithRandomErrorsAndAbandonsJob(queue, metrics, Log); - await job.RunUntilEmptyAsync(cancellationTokenSource.Token); - await cancellationTokenSource.CancelAsync(); - }); - _logger.LogInformation("Done running jobs until empty"); + var stats = await queues[i].GetQueueStatsAsync(); + if (_logger.IsEnabled(LogLevel.Information)) + _logger.LogInformation("Queue#{Id}: Working: {Working} Completed: {Completed} Abandoned: {Abandoned} Error: {Errors} Deadletter: {Deadletter}", i, stats.Working, stats.Completed, stats.Abandoned, stats.Errors, stats.Deadletter); + queueStats.Add(stats); + } + _logger.LogInformation("Done getting queue stats"); - await enqueueTask; + await metrics.FlushAsync(); + _logger.LogInformation("Done flushing metrics"); - var queueStats = new List(); - for (int i = 0; i < queues.Count; i++) - { - var stats = await queues[i].GetQueueStatsAsync(); - if (_logger.IsEnabled(LogLevel.Information)) - _logger.LogInformation("Queue#{Id}: Working: {Working} Completed: {Completed} Abandoned: {Abandoned} Error: {Errors} Deadletter: {Deadletter}", i, stats.Working, stats.Completed, stats.Abandoned, stats.Errors, stats.Deadletter); - queueStats.Add(stats); - } - _logger.LogInformation("Done getting queue stats"); - - await metrics.FlushAsync(); - _logger.LogInformation("Done flushing metrics"); - - var queueSummary = await metrics.GetQueueStatsAsync("test.samplequeueworkitem"); - Assert.Equal(queueStats.Sum(s => s.Completed), queueSummary.Completed.Count); - Assert.InRange(queueStats.Sum(s => s.Completed), 0, workItemCount); - } - finally + var queueSummary = await metrics.GetQueueStatsAsync("test.samplequeueworkitem"); + Assert.Equal(queueStats.Sum(s => s.Completed), queueSummary.Completed.Count); + Assert.InRange(queueStats.Sum(s => s.Completed), 0, workItemCount); + } + finally + { + foreach (var q in queues) { - foreach (var q in queues) - { - await q.DeleteQueueAsync(); - q.Dispose(); - } + await q.DeleteQueueAsync(); + q.Dispose(); } } } diff --git a/src/Foundatio.TestHarness/Jobs/SampleQueueJob.cs b/src/Foundatio.TestHarness/Jobs/SampleQueueJob.cs index 83ca8bd69..b7b2356af 100644 --- a/src/Foundatio.TestHarness/Jobs/SampleQueueJob.cs +++ b/src/Foundatio.TestHarness/Jobs/SampleQueueJob.cs @@ -8,114 +8,113 @@ using Foundatio.Queues; using Microsoft.Extensions.Logging; -namespace Foundatio.Tests.Jobs +namespace Foundatio.Tests.Jobs; + +public class SampleQueueWithRandomErrorsAndAbandonsJob : QueueJobBase { - public class SampleQueueWithRandomErrorsAndAbandonsJob : QueueJobBase + private readonly IMetricsClient _metrics; + + public SampleQueueWithRandomErrorsAndAbandonsJob(IQueue queue, IMetricsClient metrics, ILoggerFactory loggerFactory = null) : base(queue, loggerFactory) + { + _metrics = metrics ?? NullMetricsClient.Instance; + } + + protected override Task ProcessQueueEntryAsync(QueueEntryContext context) { - private readonly IMetricsClient _metrics; + _metrics.Counter("dequeued"); - public SampleQueueWithRandomErrorsAndAbandonsJob(IQueue queue, IMetricsClient metrics, ILoggerFactory loggerFactory = null) : base(queue, loggerFactory) + if (RandomData.GetBool(10)) { - _metrics = metrics ?? NullMetricsClient.Instance; + _metrics.Counter("errors"); + throw new Exception("Boom!"); } - protected override Task ProcessQueueEntryAsync(QueueEntryContext context) + if (RandomData.GetBool(10)) { - _metrics.Counter("dequeued"); - - if (RandomData.GetBool(10)) - { - _metrics.Counter("errors"); - throw new Exception("Boom!"); - } - - if (RandomData.GetBool(10)) - { - _metrics.Counter("abandoned"); - return Task.FromResult(JobResult.FailedWithMessage("Abandoned")); - } - - _metrics.Counter("completed"); - return Task.FromResult(JobResult.Success); + _metrics.Counter("abandoned"); + return Task.FromResult(JobResult.FailedWithMessage("Abandoned")); } + + _metrics.Counter("completed"); + return Task.FromResult(JobResult.Success); } +} + +public class SampleQueueJob : QueueJobBase +{ + private readonly IMetricsClient _metrics; - public class SampleQueueJob : QueueJobBase + public SampleQueueJob(IQueue queue, IMetricsClient metrics, ILoggerFactory loggerFactory = null) : base(queue, loggerFactory) { - private readonly IMetricsClient _metrics; + _metrics = metrics ?? NullMetricsClient.Instance; + } - public SampleQueueJob(IQueue queue, IMetricsClient metrics, ILoggerFactory loggerFactory = null) : base(queue, loggerFactory) - { - _metrics = metrics ?? NullMetricsClient.Instance; - } + protected override Task ProcessQueueEntryAsync(QueueEntryContext context) + { + _metrics.Counter("dequeued"); + _metrics.Counter("completed"); + return Task.FromResult(JobResult.Success); + } +} - protected override Task ProcessQueueEntryAsync(QueueEntryContext context) - { - _metrics.Counter("dequeued"); - _metrics.Counter("completed"); - return Task.FromResult(JobResult.Success); - } +public class SampleQueueJobWithLocking : QueueJobBase +{ + private readonly IMetricsClient _metrics; + private readonly ILockProvider _lockProvider; + + public SampleQueueJobWithLocking(IQueue queue, IMetricsClient metrics, ILockProvider lockProvider, ILoggerFactory loggerFactory = null) : base(queue, loggerFactory) + { + _metrics = metrics ?? NullMetricsClient.Instance; + _lockProvider = lockProvider; } - public class SampleQueueJobWithLocking : QueueJobBase + protected override Task GetQueueEntryLockAsync(IQueueEntry queueEntry, CancellationToken cancellationToken = default(CancellationToken)) { - private readonly IMetricsClient _metrics; - private readonly ILockProvider _lockProvider; + if (_lockProvider != null) + return _lockProvider.AcquireAsync("job", TimeSpan.FromMilliseconds(100), TimeSpan.Zero); - public SampleQueueJobWithLocking(IQueue queue, IMetricsClient metrics, ILockProvider lockProvider, ILoggerFactory loggerFactory = null) : base(queue, loggerFactory) - { - _metrics = metrics ?? NullMetricsClient.Instance; - _lockProvider = lockProvider; - } + return base.GetQueueEntryLockAsync(queueEntry, cancellationToken); + } - protected override Task GetQueueEntryLockAsync(IQueueEntry queueEntry, CancellationToken cancellationToken = default(CancellationToken)) - { - if (_lockProvider != null) - return _lockProvider.AcquireAsync("job", TimeSpan.FromMilliseconds(100), TimeSpan.Zero); + protected override Task ProcessQueueEntryAsync(QueueEntryContext context) + { + _metrics.Counter("completed"); + return Task.FromResult(JobResult.Success); + } +} - return base.GetQueueEntryLockAsync(queueEntry, cancellationToken); - } +public class SampleQueueWorkItem +{ + public string Path { get; set; } + public DateTime Created { get; set; } +} - protected override Task ProcessQueueEntryAsync(QueueEntryContext context) - { - _metrics.Counter("completed"); - return Task.FromResult(JobResult.Success); - } - } +public class SampleJob : JobBase +{ + private readonly IMetricsClient _metrics; - public class SampleQueueWorkItem + public SampleJob(IMetricsClient metrics, ILoggerFactory loggerFactory) : base(loggerFactory) { - public string Path { get; set; } - public DateTime Created { get; set; } + _metrics = metrics; } - public class SampleJob : JobBase + protected override Task RunInternalAsync(JobContext context) { - private readonly IMetricsClient _metrics; + _metrics.Counter("runs"); - public SampleJob(IMetricsClient metrics, ILoggerFactory loggerFactory) : base(loggerFactory) + if (RandomData.GetBool(10)) { - _metrics = metrics; + _metrics.Counter("errors"); + throw new Exception("Boom!"); } - protected override Task RunInternalAsync(JobContext context) + if (RandomData.GetBool(10)) { - _metrics.Counter("runs"); - - if (RandomData.GetBool(10)) - { - _metrics.Counter("errors"); - throw new Exception("Boom!"); - } - - if (RandomData.GetBool(10)) - { - _metrics.Counter("failed"); - return Task.FromResult(JobResult.FailedWithMessage("Failed")); - } - - _metrics.Counter("completed"); - return Task.FromResult(JobResult.Success); + _metrics.Counter("failed"); + return Task.FromResult(JobResult.FailedWithMessage("Failed")); } + + _metrics.Counter("completed"); + return Task.FromResult(JobResult.Success); } } diff --git a/src/Foundatio.TestHarness/Jobs/ThrottledJob.cs b/src/Foundatio.TestHarness/Jobs/ThrottledJob.cs index 9e426f4b3..3a600c8a7 100644 --- a/src/Foundatio.TestHarness/Jobs/ThrottledJob.cs +++ b/src/Foundatio.TestHarness/Jobs/ThrottledJob.cs @@ -6,28 +6,27 @@ using Foundatio.Lock; using Microsoft.Extensions.Logging; -namespace Foundatio.Tests.Jobs +namespace Foundatio.Tests.Jobs; + +public class ThrottledJob : JobWithLockBase { - public class ThrottledJob : JobWithLockBase + public ThrottledJob(ICacheClient client, ILoggerFactory loggerFactory = null) : base(loggerFactory) { - public ThrottledJob(ICacheClient client, ILoggerFactory loggerFactory = null) : base(loggerFactory) - { - _locker = new ThrottlingLockProvider(client, 1, TimeSpan.FromMilliseconds(100), loggerFactory); - } + _locker = new ThrottlingLockProvider(client, 1, TimeSpan.FromMilliseconds(100), loggerFactory); + } - private readonly ILockProvider _locker; - public int RunCount { get; set; } + private readonly ILockProvider _locker; + public int RunCount { get; set; } - protected override Task GetLockAsync(CancellationToken cancellationToken = default(CancellationToken)) - { - return _locker.AcquireAsync(nameof(ThrottledJob), acquireTimeout: TimeSpan.Zero); - } + protected override Task GetLockAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + return _locker.AcquireAsync(nameof(ThrottledJob), acquireTimeout: TimeSpan.Zero); + } - protected override Task RunInternalAsync(JobContext context) - { - RunCount++; + protected override Task RunInternalAsync(JobContext context) + { + RunCount++; - return Task.FromResult(JobResult.Success); - } + return Task.FromResult(JobResult.Success); } } diff --git a/src/Foundatio.TestHarness/Jobs/WithDependencyJob.cs b/src/Foundatio.TestHarness/Jobs/WithDependencyJob.cs index c4c0f7611..2dcb855f2 100644 --- a/src/Foundatio.TestHarness/Jobs/WithDependencyJob.cs +++ b/src/Foundatio.TestHarness/Jobs/WithDependencyJob.cs @@ -2,29 +2,28 @@ using Foundatio.Jobs; using Microsoft.Extensions.Logging; -namespace Foundatio.Tests.Jobs +namespace Foundatio.Tests.Jobs; + +public class WithDependencyJob : JobBase { - public class WithDependencyJob : JobBase + public WithDependencyJob(MyDependency dependency, ILoggerFactory loggerFactory = null) : base(loggerFactory) { - public WithDependencyJob(MyDependency dependency, ILoggerFactory loggerFactory = null) : base(loggerFactory) - { - Dependency = dependency; - } + Dependency = dependency; + } - public MyDependency Dependency { get; private set; } + public MyDependency Dependency { get; private set; } - public int RunCount { get; set; } + public int RunCount { get; set; } - protected override Task RunInternalAsync(JobContext context) - { - RunCount++; + protected override Task RunInternalAsync(JobContext context) + { + RunCount++; - return Task.FromResult(JobResult.Success); - } + return Task.FromResult(JobResult.Success); } +} - public class MyDependency - { - public int MyProperty { get; set; } - } +public class MyDependency +{ + public int MyProperty { get; set; } } diff --git a/src/Foundatio.TestHarness/Jobs/WithLockingJob.cs b/src/Foundatio.TestHarness/Jobs/WithLockingJob.cs index da8073a8b..d92bfaadf 100644 --- a/src/Foundatio.TestHarness/Jobs/WithLockingJob.cs +++ b/src/Foundatio.TestHarness/Jobs/WithLockingJob.cs @@ -9,32 +9,31 @@ using Microsoft.Extensions.Logging; using Xunit; -namespace Foundatio.Tests.Jobs +namespace Foundatio.Tests.Jobs; + +public class WithLockingJob : JobWithLockBase { - public class WithLockingJob : JobWithLockBase - { - private readonly ILockProvider _locker; + private readonly ILockProvider _locker; - public WithLockingJob(ILoggerFactory loggerFactory) : base(loggerFactory) - { - _locker = new CacheLockProvider(new InMemoryCacheClient(new InMemoryCacheClientOptions { LoggerFactory = loggerFactory }), new InMemoryMessageBus(new InMemoryMessageBusOptions { LoggerFactory = loggerFactory }), loggerFactory); - } + public WithLockingJob(ILoggerFactory loggerFactory) : base(loggerFactory) + { + _locker = new CacheLockProvider(new InMemoryCacheClient(new InMemoryCacheClientOptions { LoggerFactory = loggerFactory }), new InMemoryMessageBus(new InMemoryMessageBusOptions { LoggerFactory = loggerFactory }), loggerFactory); + } - public int RunCount { get; set; } + public int RunCount { get; set; } - protected override Task GetLockAsync(CancellationToken cancellationToken = default(CancellationToken)) - { - return _locker.AcquireAsync(nameof(WithLockingJob), TimeSpan.FromSeconds(1), TimeSpan.Zero); - } + protected override Task GetLockAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + return _locker.AcquireAsync(nameof(WithLockingJob), TimeSpan.FromSeconds(1), TimeSpan.Zero); + } - protected override async Task RunInternalAsync(JobContext context) - { - RunCount++; + protected override async Task RunInternalAsync(JobContext context) + { + RunCount++; - await SystemClock.SleepAsync(150, context.CancellationToken); - Assert.True(await _locker.IsLockedAsync("WithLockingJob")); + await SystemClock.SleepAsync(150, context.CancellationToken); + Assert.True(await _locker.IsLockedAsync("WithLockingJob")); - return JobResult.Success; - } + return JobResult.Success; } } diff --git a/src/Foundatio.TestHarness/Locks/LockTestBase.cs b/src/Foundatio.TestHarness/Locks/LockTestBase.cs index 60ca14d38..01d7513ae 100644 --- a/src/Foundatio.TestHarness/Locks/LockTestBase.cs +++ b/src/Foundatio.TestHarness/Locks/LockTestBase.cs @@ -12,305 +12,304 @@ using Xunit; using Xunit.Abstractions; -namespace Foundatio.Tests.Locks +namespace Foundatio.Tests.Locks; + +public abstract class LockTestBase : TestWithLoggingBase { - public abstract class LockTestBase : TestWithLoggingBase + protected LockTestBase(ITestOutputHelper output) : base(output) { } + + protected virtual ILockProvider GetThrottlingLockProvider(int maxHits, TimeSpan period) { - protected LockTestBase(ITestOutputHelper output) : base(output) { } + return null; + } - protected virtual ILockProvider GetThrottlingLockProvider(int maxHits, TimeSpan period) - { - return null; - } + protected virtual ILockProvider GetLockProvider() + { + return null; + } + + public virtual async Task CanAcquireAndReleaseLockAsync() + { + Log.SetLogLevel(LogLevel.Trace); - protected virtual ILockProvider GetLockProvider() + var locker = GetLockProvider(); + if (locker == null) + return; + + var lock1 = await locker.AcquireAsync("test", acquireTimeout: TimeSpan.FromMilliseconds(100), timeUntilExpires: TimeSpan.FromSeconds(1)); + + try { - return null; + Assert.NotNull(lock1); + Assert.True(await locker.IsLockedAsync("test")); + var lock2Task = locker.AcquireAsync("test", acquireTimeout: TimeSpan.FromMilliseconds(250)); + await SystemClock.SleepAsync(TimeSpan.FromMilliseconds(250)); + Assert.Null(await lock2Task); } - - public virtual async Task CanAcquireAndReleaseLockAsync() + finally { - Log.SetLogLevel(LogLevel.Trace); + await lock1.ReleaseAsync(); + } - var locker = GetLockProvider(); - if (locker == null) - return; + Assert.False(await locker.IsLockedAsync("test")); - var lock1 = await locker.AcquireAsync("test", acquireTimeout: TimeSpan.FromMilliseconds(100), timeUntilExpires: TimeSpan.FromSeconds(1)); + int counter = 0; - try - { - Assert.NotNull(lock1); - Assert.True(await locker.IsLockedAsync("test")); - var lock2Task = locker.AcquireAsync("test", acquireTimeout: TimeSpan.FromMilliseconds(250)); - await SystemClock.SleepAsync(TimeSpan.FromMilliseconds(250)); - Assert.Null(await lock2Task); - } - finally + bool isTraceLogLevelEnabled = _logger.IsEnabled(LogLevel.Trace); + await Run.InParallelAsync(25, async i => + { + bool success = await locker.TryUsingAsync("test", () => { - await lock1.ReleaseAsync(); - } + Interlocked.Increment(ref counter); + }, acquireTimeout: TimeSpan.FromSeconds(10)); - Assert.False(await locker.IsLockedAsync("test")); + Assert.True(success); + }); - int counter = 0; + Assert.Equal(25, counter); + } - bool isTraceLogLevelEnabled = _logger.IsEnabled(LogLevel.Trace); - await Run.InParallelAsync(25, async i => - { - bool success = await locker.TryUsingAsync("test", () => - { - Interlocked.Increment(ref counter); - }, acquireTimeout: TimeSpan.FromSeconds(10)); + public virtual async Task CanReleaseLockMultipleTimes() + { + var locker = GetLockProvider(); + if (locker == null) + return; - Assert.True(success); - }); + var lock1 = await locker.AcquireAsync("test", acquireTimeout: TimeSpan.FromMilliseconds(100), timeUntilExpires: TimeSpan.FromSeconds(1)); + await lock1.ReleaseAsync(); + Assert.False(await locker.IsLockedAsync("test")); - Assert.Equal(25, counter); - } + var lock2 = await locker.AcquireAsync("test", acquireTimeout: TimeSpan.FromMilliseconds(100), timeUntilExpires: TimeSpan.FromSeconds(1)); - public virtual async Task CanReleaseLockMultipleTimes() - { - var locker = GetLockProvider(); - if (locker == null) - return; + // has already been released, should not release other people's lock + await lock1.ReleaseAsync(); + Assert.True(await locker.IsLockedAsync("test")); - var lock1 = await locker.AcquireAsync("test", acquireTimeout: TimeSpan.FromMilliseconds(100), timeUntilExpires: TimeSpan.FromSeconds(1)); - await lock1.ReleaseAsync(); - Assert.False(await locker.IsLockedAsync("test")); + // has already been released, should not release other people's lock + await lock1.DisposeAsync(); + Assert.True(await locker.IsLockedAsync("test")); - var lock2 = await locker.AcquireAsync("test", acquireTimeout: TimeSpan.FromMilliseconds(100), timeUntilExpires: TimeSpan.FromSeconds(1)); + await lock2.ReleaseAsync(); + Assert.False(await locker.IsLockedAsync("test")); + } - // has already been released, should not release other people's lock - await lock1.ReleaseAsync(); - Assert.True(await locker.IsLockedAsync("test")); + public virtual async Task LockWillTimeoutAsync() + { + Log.SetLogLevel(LogLevel.Trace); + Log.SetLogLevel(LogLevel.Trace); + Log.SetLogLevel(LogLevel.Trace); + + var locker = GetLockProvider(); + if (locker == null) + return; + + _logger.LogInformation("Acquiring lock #1"); + var testLock = await locker.AcquireAsync("test", timeUntilExpires: TimeSpan.FromMilliseconds(250)); + _logger.LogInformation(testLock != null ? "Acquired lock #1" : "Unable to acquire lock #1"); + Assert.NotNull(testLock); + + _logger.LogInformation("Acquiring lock #2"); + testLock = await locker.AcquireAsync("test", acquireTimeout: TimeSpan.FromMilliseconds(50)); + _logger.LogInformation(testLock != null ? "Acquired lock #2" : "Unable to acquire lock #2"); + Assert.Null(testLock); + + _logger.LogInformation("Acquiring lock #3"); + testLock = await locker.AcquireAsync("test", acquireTimeout: TimeSpan.FromSeconds(10)); + _logger.LogInformation(testLock != null ? "Acquired lock #3" : "Unable to acquire lock #3"); + Assert.NotNull(testLock); + } - // has already been released, should not release other people's lock - await lock1.DisposeAsync(); - Assert.True(await locker.IsLockedAsync("test")); + public virtual async Task CanAcquireMultipleResources() + { + Log.SetLogLevel(LogLevel.Trace); + Log.SetLogLevel(LogLevel.Trace); + Log.SetLogLevel(LogLevel.Trace); + + var locker = GetLockProvider(); + if (locker == null) + return; + + var resources = new List { "test1", "test2", "test3", "test4", "test5" }; + var testLock = await locker.AcquireAsync(resources, timeUntilExpires: TimeSpan.FromMilliseconds(250)); + _logger.LogInformation(testLock != null ? "Acquired lock #1" : "Unable to acquire lock #1"); + Assert.NotNull(testLock); + + resources.Add("other"); + var testLock2 = await locker.AcquireAsync(resources, timeUntilExpires: TimeSpan.FromMilliseconds(250), acquireTimeout: TimeSpan.FromMilliseconds(10)); + _logger.LogInformation(testLock2 != null ? "Acquired lock #1" : "Unable to acquire lock #1"); + Assert.Null(testLock2); + + await testLock.RenewAsync(); + await testLock.ReleaseAsync(); + + var testLock3 = await locker.AcquireAsync(resources, timeUntilExpires: TimeSpan.FromMilliseconds(250), acquireTimeout: TimeSpan.FromMilliseconds(10)); + _logger.LogInformation(testLock3 != null ? "Acquired lock #1" : "Unable to acquire lock #1"); + Assert.NotNull(testLock3); + } - await lock2.ReleaseAsync(); - Assert.False(await locker.IsLockedAsync("test")); - } + public virtual async Task CanAcquireMultipleScopedResources() + { + Log.SetLogLevel(LogLevel.Trace); + Log.SetLogLevel(LogLevel.Trace); + Log.SetLogLevel(LogLevel.Trace); - public virtual async Task LockWillTimeoutAsync() - { - Log.SetLogLevel(LogLevel.Trace); - Log.SetLogLevel(LogLevel.Trace); - Log.SetLogLevel(LogLevel.Trace); - - var locker = GetLockProvider(); - if (locker == null) - return; - - _logger.LogInformation("Acquiring lock #1"); - var testLock = await locker.AcquireAsync("test", timeUntilExpires: TimeSpan.FromMilliseconds(250)); - _logger.LogInformation(testLock != null ? "Acquired lock #1" : "Unable to acquire lock #1"); - Assert.NotNull(testLock); - - _logger.LogInformation("Acquiring lock #2"); - testLock = await locker.AcquireAsync("test", acquireTimeout: TimeSpan.FromMilliseconds(50)); - _logger.LogInformation(testLock != null ? "Acquired lock #2" : "Unable to acquire lock #2"); - Assert.Null(testLock); - - _logger.LogInformation("Acquiring lock #3"); - testLock = await locker.AcquireAsync("test", acquireTimeout: TimeSpan.FromSeconds(10)); - _logger.LogInformation(testLock != null ? "Acquired lock #3" : "Unable to acquire lock #3"); - Assert.NotNull(testLock); - } + var locker = GetLockProvider(); + if (locker == null) + return; - public virtual async Task CanAcquireMultipleResources() - { - Log.SetLogLevel(LogLevel.Trace); - Log.SetLogLevel(LogLevel.Trace); - Log.SetLogLevel(LogLevel.Trace); - - var locker = GetLockProvider(); - if (locker == null) - return; - - var resources = new List { "test1", "test2", "test3", "test4", "test5" }; - var testLock = await locker.AcquireAsync(resources, timeUntilExpires: TimeSpan.FromMilliseconds(250)); - _logger.LogInformation(testLock != null ? "Acquired lock #1" : "Unable to acquire lock #1"); - Assert.NotNull(testLock); - - resources.Add("other"); - var testLock2 = await locker.AcquireAsync(resources, timeUntilExpires: TimeSpan.FromMilliseconds(250), acquireTimeout: TimeSpan.FromMilliseconds(10)); - _logger.LogInformation(testLock2 != null ? "Acquired lock #1" : "Unable to acquire lock #1"); - Assert.Null(testLock2); - - await testLock.RenewAsync(); - await testLock.ReleaseAsync(); - - var testLock3 = await locker.AcquireAsync(resources, timeUntilExpires: TimeSpan.FromMilliseconds(250), acquireTimeout: TimeSpan.FromMilliseconds(10)); - _logger.LogInformation(testLock3 != null ? "Acquired lock #1" : "Unable to acquire lock #1"); - Assert.NotNull(testLock3); - } + locker = new ScopedLockProvider(locker, "myscope"); - public virtual async Task CanAcquireMultipleScopedResources() - { - Log.SetLogLevel(LogLevel.Trace); - Log.SetLogLevel(LogLevel.Trace); - Log.SetLogLevel(LogLevel.Trace); + var resources = new List { "test1", "test2", "test3", "test4", "test5" }; + var testLock = await locker.AcquireAsync(resources, timeUntilExpires: TimeSpan.FromMilliseconds(250)); + _logger.LogInformation(testLock != null ? "Acquired lock #1" : "Unable to acquire lock #1"); + Assert.NotNull(testLock); - var locker = GetLockProvider(); - if (locker == null) - return; + resources.Add("other"); + var testLock2 = await locker.AcquireAsync(resources, timeUntilExpires: TimeSpan.FromMilliseconds(250), acquireTimeout: TimeSpan.FromMilliseconds(10)); + _logger.LogInformation(testLock2 != null ? "Acquired lock #1" : "Unable to acquire lock #1"); + Assert.Null(testLock2); - locker = new ScopedLockProvider(locker, "myscope"); + await testLock.RenewAsync(); + await testLock.ReleaseAsync(); - var resources = new List { "test1", "test2", "test3", "test4", "test5" }; - var testLock = await locker.AcquireAsync(resources, timeUntilExpires: TimeSpan.FromMilliseconds(250)); - _logger.LogInformation(testLock != null ? "Acquired lock #1" : "Unable to acquire lock #1"); - Assert.NotNull(testLock); + var testLock3 = await locker.AcquireAsync(resources, timeUntilExpires: TimeSpan.FromMilliseconds(250), acquireTimeout: TimeSpan.FromMilliseconds(10)); + _logger.LogInformation(testLock3 != null ? "Acquired lock #1" : "Unable to acquire lock #1"); + Assert.NotNull(testLock3); + } - resources.Add("other"); - var testLock2 = await locker.AcquireAsync(resources, timeUntilExpires: TimeSpan.FromMilliseconds(250), acquireTimeout: TimeSpan.FromMilliseconds(10)); - _logger.LogInformation(testLock2 != null ? "Acquired lock #1" : "Unable to acquire lock #1"); - Assert.Null(testLock2); + public virtual async Task CanAcquireLocksInParallel() + { + var locker = GetLockProvider(); + if (locker == null) + return; - await testLock.RenewAsync(); - await testLock.ReleaseAsync(); + Log.SetLogLevel(LogLevel.Debug); - var testLock3 = await locker.AcquireAsync(resources, timeUntilExpires: TimeSpan.FromMilliseconds(250), acquireTimeout: TimeSpan.FromMilliseconds(10)); - _logger.LogInformation(testLock3 != null ? "Acquired lock #1" : "Unable to acquire lock #1"); - Assert.NotNull(testLock3); - } + const int COUNT = 100; + int current = 1; + var used = new List(); + int concurrency = 0; - public virtual async Task CanAcquireLocksInParallel() + await Parallel.ForEachAsync(Enumerable.Range(1, COUNT), async (index, ct) => { - var locker = GetLockProvider(); - if (locker == null) - return; - - Log.SetLogLevel(LogLevel.Debug); + await using var myLock = await locker.AcquireAsync("test", TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1)); + Assert.NotNull(myLock); - const int COUNT = 100; - int current = 1; - var used = new List(); - int concurrency = 0; + int currentConcurrency = Interlocked.Increment(ref concurrency); + Assert.Equal(1, currentConcurrency); - await Parallel.ForEachAsync(Enumerable.Range(1, COUNT), async (index, ct) => - { - await using var myLock = await locker.AcquireAsync("test", TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1)); - Assert.NotNull(myLock); + int item = current; + await Task.Delay(10, ct); + used.Add(item); + current++; - int currentConcurrency = Interlocked.Increment(ref concurrency); - Assert.Equal(1, currentConcurrency); + Interlocked.Decrement(ref concurrency); + }); - int item = current; - await Task.Delay(10, ct); - used.Add(item); - current++; + var duplicates = used.GroupBy(x => x).Where(g => g.Count() > 1); + Assert.Empty(duplicates); + Assert.Equal(COUNT, used.Count); + } - Interlocked.Decrement(ref concurrency); - }); + public virtual async Task LockOneAtATimeAsync() + { + var locker = GetLockProvider(); + if (locker == null) + return; - var duplicates = used.GroupBy(x => x).Where(g => g.Count() > 1); - Assert.Empty(duplicates); - Assert.Equal(COUNT, used.Count); - } + Log.SetLogLevel(LogLevel.Trace); - public virtual async Task LockOneAtATimeAsync() + int successCount = 0; + var lockTask1 = Task.Run(async () => { - var locker = GetLockProvider(); - if (locker == null) - return; - - Log.SetLogLevel(LogLevel.Trace); - - int successCount = 0; - var lockTask1 = Task.Run(async () => + if (await DoLockedWorkAsync(locker)) { - if (await DoLockedWorkAsync(locker)) - { - Interlocked.Increment(ref successCount); - _logger.LogInformation("LockTask1 Success"); - } - }); - var lockTask2 = Task.Run(async () => + Interlocked.Increment(ref successCount); + _logger.LogInformation("LockTask1 Success"); + } + }); + var lockTask2 = Task.Run(async () => + { + if (await DoLockedWorkAsync(locker)) { - if (await DoLockedWorkAsync(locker)) - { - Interlocked.Increment(ref successCount); - _logger.LogInformation("LockTask2 Success"); - } - }); - var lockTask3 = Task.Run(async () => + Interlocked.Increment(ref successCount); + _logger.LogInformation("LockTask2 Success"); + } + }); + var lockTask3 = Task.Run(async () => + { + if (await DoLockedWorkAsync(locker)) { - if (await DoLockedWorkAsync(locker)) - { - Interlocked.Increment(ref successCount); - _logger.LogInformation("LockTask3 Success"); - } - }); - var lockTask4 = Task.Run(async () => + Interlocked.Increment(ref successCount); + _logger.LogInformation("LockTask3 Success"); + } + }); + var lockTask4 = Task.Run(async () => + { + if (await DoLockedWorkAsync(locker)) { - if (await DoLockedWorkAsync(locker)) - { - Interlocked.Increment(ref successCount); - _logger.LogInformation("LockTask4 Success"); - } - }); + Interlocked.Increment(ref successCount); + _logger.LogInformation("LockTask4 Success"); + } + }); - await Task.WhenAll(lockTask1, lockTask2, lockTask3, lockTask4); - Assert.Equal(1, successCount); + await Task.WhenAll(lockTask1, lockTask2, lockTask3, lockTask4); + Assert.Equal(1, successCount); - await Task.Run(async () => - { - if (await DoLockedWorkAsync(locker)) - Interlocked.Increment(ref successCount); - }); - Assert.Equal(2, successCount); - } - - private Task DoLockedWorkAsync(ILockProvider locker) + await Task.Run(async () => { - return locker.TryUsingAsync("DoLockedWork", async () => await SystemClock.SleepAsync(500), TimeSpan.FromMinutes(1), TimeSpan.Zero); - } + if (await DoLockedWorkAsync(locker)) + Interlocked.Increment(ref successCount); + }); + Assert.Equal(2, successCount); + } - public virtual async Task WillThrottleCallsAsync() - { - Log.MinimumLevel = LogLevel.Trace; - Log.SetLogLevel(LogLevel.Information); - Log.SetLogLevel(LogLevel.Trace); + private Task DoLockedWorkAsync(ILockProvider locker) + { + return locker.TryUsingAsync("DoLockedWork", async () => await SystemClock.SleepAsync(500), TimeSpan.FromMinutes(1), TimeSpan.Zero); + } + + public virtual async Task WillThrottleCallsAsync() + { + Log.MinimumLevel = LogLevel.Trace; + Log.SetLogLevel(LogLevel.Information); + Log.SetLogLevel(LogLevel.Trace); - const int allowedLocks = 25; + const int allowedLocks = 25; - var period = TimeSpan.FromSeconds(2); - var locker = GetThrottlingLockProvider(allowedLocks, period); - if (locker == null) - return; + var period = TimeSpan.FromSeconds(2); + var locker = GetThrottlingLockProvider(allowedLocks, period); + if (locker == null) + return; - string lockName = Guid.NewGuid().ToString("N").Substring(0, 10); + string lockName = Guid.NewGuid().ToString("N").Substring(0, 10); - // sleep until start of throttling period - while (SystemClock.UtcNow.Ticks % period.Ticks < TimeSpan.TicksPerMillisecond * 100) - Thread.Sleep(10); + // sleep until start of throttling period + while (SystemClock.UtcNow.Ticks % period.Ticks < TimeSpan.TicksPerMillisecond * 100) + Thread.Sleep(10); - var sw = Stopwatch.StartNew(); - for (int i = 1; i <= allowedLocks; i++) - { - _logger.LogInformation("Allowed Locks: {Id}", i); - var l = await locker.AcquireAsync(lockName); - Assert.NotNull(l); - } - sw.Stop(); - - _logger.LogInformation("Time to acquire {AllowedLocks} locks: {Elapsed:g}", allowedLocks, sw.Elapsed); - Assert.True(sw.Elapsed.TotalSeconds < 1); - - sw.Restart(); - var result = await locker.AcquireAsync(lockName, cancellationToken: new CancellationToken(true)); - sw.Stop(); - _logger.LogInformation("Total acquire time took to attempt to get throttled lock: {Elapsed:g}", sw.Elapsed); - Assert.Null(result); - - sw.Restart(); - result = await locker.AcquireAsync(lockName, acquireTimeout: TimeSpan.FromSeconds(2.5)); - sw.Stop(); - _logger.LogInformation("Time to acquire lock: {Elapsed:g}", sw.Elapsed); - Assert.NotNull(result); + var sw = Stopwatch.StartNew(); + for (int i = 1; i <= allowedLocks; i++) + { + _logger.LogInformation("Allowed Locks: {Id}", i); + var l = await locker.AcquireAsync(lockName); + Assert.NotNull(l); } + sw.Stop(); + + _logger.LogInformation("Time to acquire {AllowedLocks} locks: {Elapsed:g}", allowedLocks, sw.Elapsed); + Assert.True(sw.Elapsed.TotalSeconds < 1); + + sw.Restart(); + var result = await locker.AcquireAsync(lockName, cancellationToken: new CancellationToken(true)); + sw.Stop(); + _logger.LogInformation("Total acquire time took to attempt to get throttled lock: {Elapsed:g}", sw.Elapsed); + Assert.Null(result); + + sw.Restart(); + result = await locker.AcquireAsync(lockName, acquireTimeout: TimeSpan.FromSeconds(2.5)); + sw.Stop(); + _logger.LogInformation("Time to acquire lock: {Elapsed:g}", sw.Elapsed); + Assert.NotNull(result); } } diff --git a/src/Foundatio.TestHarness/Messaging/MessageBusTestBase.cs b/src/Foundatio.TestHarness/Messaging/MessageBusTestBase.cs index 3819ae95e..fc68da431 100644 --- a/src/Foundatio.TestHarness/Messaging/MessageBusTestBase.cs +++ b/src/Foundatio.TestHarness/Messaging/MessageBusTestBase.cs @@ -14,712 +14,711 @@ using Xunit; using Xunit.Abstractions; -namespace Foundatio.Tests.Messaging +namespace Foundatio.Tests.Messaging; + +public abstract class MessageBusTestBase : TestWithLoggingBase { - public abstract class MessageBusTestBase : TestWithLoggingBase + protected MessageBusTestBase(ITestOutputHelper output) : base(output) { - protected MessageBusTestBase(ITestOutputHelper output) : base(output) - { - Log.SetLogLevel(LogLevel.Debug); - } + Log.SetLogLevel(LogLevel.Debug); + } - protected virtual IMessageBus GetMessageBus(Func config = null) - { - return null; - } + protected virtual IMessageBus GetMessageBus(Func config = null) + { + return null; + } - protected virtual Task CleanupMessageBusAsync(IMessageBus messageBus) - { - messageBus?.Dispose(); - return Task.CompletedTask; - } + protected virtual Task CleanupMessageBusAsync(IMessageBus messageBus) + { + messageBus?.Dispose(); + return Task.CompletedTask; + } - public virtual async Task CanUseMessageOptionsAsync() - { - var messageBus = GetMessageBus(); - if (messageBus == null) - return; + public virtual async Task CanUseMessageOptionsAsync() + { + var messageBus = GetMessageBus(); + if (messageBus == null) + return; - using var metricsCollector = new DiagnosticsMetricsCollector(FoundatioDiagnostics.Meter.Name, _logger); + using var metricsCollector = new DiagnosticsMetricsCollector(FoundatioDiagnostics.Meter.Name, _logger); - try + try + { + using var listener = new ActivityListener { - using var listener = new ActivityListener - { - ShouldListenTo = s => s.Name == FoundatioDiagnostics.ActivitySource.Name, - Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded, - ActivityStarted = activity => _logger.LogInformation("Start: " + activity.DisplayName), - ActivityStopped = activity => _logger.LogInformation("Stop: " + activity.DisplayName) - }; + ShouldListenTo = s => s.Name == FoundatioDiagnostics.ActivitySource.Name, + Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded, + ActivityStarted = activity => _logger.LogInformation("Start: " + activity.DisplayName), + ActivityStopped = activity => _logger.LogInformation("Stop: " + activity.DisplayName) + }; - ActivitySource.AddActivityListener(listener); + ActivitySource.AddActivityListener(listener); - using var activity = FoundatioDiagnostics.ActivitySource.StartActivity("Parent", ActivityKind.Internal); - Assert.Equal(Activity.Current, activity); + using var activity = FoundatioDiagnostics.ActivitySource.StartActivity("Parent", ActivityKind.Internal); + Assert.Equal(Activity.Current, activity); - var countdown = new AsyncCountdownEvent(1); - await messageBus.SubscribeAsync>(msg => - { - _logger.LogTrace("Got message"); + var countdown = new AsyncCountdownEvent(1); + await messageBus.SubscribeAsync>(msg => + { + _logger.LogTrace("Got message"); - Assert.Equal("Hello", msg.Body.Data); - Assert.True(msg.Body.Items.ContainsKey("Test")); + Assert.Equal("Hello", msg.Body.Data); + Assert.True(msg.Body.Items.ContainsKey("Test")); - Assert.Equal(activity.Id, msg.CorrelationId); - Assert.Equal(Activity.Current.ParentId, activity.Id); - Assert.Single(msg.Properties); - Assert.Contains(msg.Properties, i => i.Key == "hey" && i.Value.ToString() == "now"); - countdown.Signal(); - _logger.LogTrace("Set event"); - }); - - await SystemClock.SleepAsync(1000); - await messageBus.PublishAsync(new SimpleMessageA - { - Data = "Hello", - Items = { { "Test", "Test" } } - }, new MessageOptions - { - Properties = new Dictionary { - { "hey", "now" } - } - }); - _logger.LogTrace("Published one..."); + Assert.Equal(activity.Id, msg.CorrelationId); + Assert.Equal(Activity.Current.ParentId, activity.Id); + Assert.Single(msg.Properties); + Assert.Contains(msg.Properties, i => i.Key == "hey" && i.Value.ToString() == "now"); + countdown.Signal(); + _logger.LogTrace("Set event"); + }); - await countdown.WaitAsync(TimeSpan.FromSeconds(5)); - Assert.Equal(0, countdown.CurrentCount); - } - finally + await SystemClock.SleepAsync(1000); + await messageBus.PublishAsync(new SimpleMessageA { - await CleanupMessageBusAsync(messageBus); - } - } + Data = "Hello", + Items = { { "Test", "Test" } } + }, new MessageOptions + { + Properties = new Dictionary { + { "hey", "now" } + } + }); + _logger.LogTrace("Published one..."); - public virtual async Task CanSendMessageAsync() + await countdown.WaitAsync(TimeSpan.FromSeconds(5)); + Assert.Equal(0, countdown.CurrentCount); + } + finally { - var messageBus = GetMessageBus(); - if (messageBus == null) - return; + await CleanupMessageBusAsync(messageBus); + } + } - try - { - var countdown = new AsyncCountdownEvent(1); - await messageBus.SubscribeAsync(msg => - { - _logger.LogTrace("Got message"); - Assert.Equal("Hello", msg.Data); - Assert.True(msg.Items.ContainsKey("Test")); - countdown.Signal(); - _logger.LogTrace("Set event"); - }); + public virtual async Task CanSendMessageAsync() + { + var messageBus = GetMessageBus(); + if (messageBus == null) + return; - await SystemClock.SleepAsync(100); - await messageBus.PublishAsync(new SimpleMessageA - { - Data = "Hello", - Items = { { "Test", "Test" } } - }); - _logger.LogTrace("Published one..."); + try + { + var countdown = new AsyncCountdownEvent(1); + await messageBus.SubscribeAsync(msg => + { + _logger.LogTrace("Got message"); + Assert.Equal("Hello", msg.Data); + Assert.True(msg.Items.ContainsKey("Test")); + countdown.Signal(); + _logger.LogTrace("Set event"); + }); - await countdown.WaitAsync(TimeSpan.FromSeconds(5)); - Assert.Equal(0, countdown.CurrentCount); - } - finally + await SystemClock.SleepAsync(100); + await messageBus.PublishAsync(new SimpleMessageA { - await CleanupMessageBusAsync(messageBus); - } - } + Data = "Hello", + Items = { { "Test", "Test" } } + }); + _logger.LogTrace("Published one..."); - public virtual async Task CanHandleNullMessageAsync() + await countdown.WaitAsync(TimeSpan.FromSeconds(5)); + Assert.Equal(0, countdown.CurrentCount); + } + finally { - var messageBus = GetMessageBus(); - if (messageBus == null) - return; + await CleanupMessageBusAsync(messageBus); + } + } - try + public virtual async Task CanHandleNullMessageAsync() + { + var messageBus = GetMessageBus(); + if (messageBus == null) + return; + + try + { + var countdown = new AsyncCountdownEvent(1); + await messageBus.SubscribeAsync(msg => { - var countdown = new AsyncCountdownEvent(1); - await messageBus.SubscribeAsync(msg => - { - countdown.Signal(); - throw new Exception(); - }); + countdown.Signal(); + throw new Exception(); + }); - await SystemClock.SleepAsync(100); - await messageBus.PublishAsync(null); - _logger.LogTrace("Published one..."); + await SystemClock.SleepAsync(100); + await messageBus.PublishAsync(null); + _logger.LogTrace("Published one..."); - await countdown.WaitAsync(TimeSpan.FromSeconds(1)); - Assert.Equal(1, countdown.CurrentCount); - } - finally - { - await CleanupMessageBusAsync(messageBus); - } + await countdown.WaitAsync(TimeSpan.FromSeconds(1)); + Assert.Equal(1, countdown.CurrentCount); } - - public virtual async Task CanSendDerivedMessageAsync() + finally { - var messageBus = GetMessageBus(); - if (messageBus == null) - return; - - try - { - var countdown = new AsyncCountdownEvent(1); - await messageBus.SubscribeAsync(msg => - { - _logger.LogTrace("Got message"); - Assert.Equal("Hello", msg.Data); - countdown.Signal(); - _logger.LogTrace("Set event"); - }); - - await SystemClock.SleepAsync(100); - await messageBus.PublishAsync(new DerivedSimpleMessageA - { - Data = "Hello" - }); - _logger.LogTrace("Published one..."); - await countdown.WaitAsync(TimeSpan.FromSeconds(5)); - Assert.Equal(0, countdown.CurrentCount); - } - finally - { - await CleanupMessageBusAsync(messageBus); - } + await CleanupMessageBusAsync(messageBus); } + } + + public virtual async Task CanSendDerivedMessageAsync() + { + var messageBus = GetMessageBus(); + if (messageBus == null) + return; - public virtual async Task CanSendMappedMessageAsync() + try { - var messageBus = GetMessageBus(b => + var countdown = new AsyncCountdownEvent(1); + await messageBus.SubscribeAsync(msg => { - b.MessageTypeMappings.Add(nameof(SimpleMessageA), typeof(SimpleMessageA)); - return b; + _logger.LogTrace("Got message"); + Assert.Equal("Hello", msg.Data); + countdown.Signal(); + _logger.LogTrace("Set event"); }); - if (messageBus == null) - return; - try + await SystemClock.SleepAsync(100); + await messageBus.PublishAsync(new DerivedSimpleMessageA { - var countdown = new AsyncCountdownEvent(1); - await messageBus.SubscribeAsync(msg => - { - _logger.LogTrace("Got message"); - Assert.Equal("Hello", msg.Data); - countdown.Signal(); - _logger.LogTrace("Set event"); - }); - - await SystemClock.SleepAsync(100); - await messageBus.PublishAsync(new SimpleMessageA - { - Data = "Hello" - }); - _logger.LogTrace("Published one..."); - await countdown.WaitAsync(TimeSpan.FromSeconds(5)); - Assert.Equal(0, countdown.CurrentCount); - } - finally - { - await CleanupMessageBusAsync(messageBus); - } + Data = "Hello" + }); + _logger.LogTrace("Published one..."); + await countdown.WaitAsync(TimeSpan.FromSeconds(5)); + Assert.Equal(0, countdown.CurrentCount); + } + finally + { + await CleanupMessageBusAsync(messageBus); } + } - public virtual async Task CanSendDelayedMessageAsync() + public virtual async Task CanSendMappedMessageAsync() + { + var messageBus = GetMessageBus(b => { - const int numConcurrentMessages = 1000; - var messageBus = GetMessageBus(); - if (messageBus == null) - return; + b.MessageTypeMappings.Add(nameof(SimpleMessageA), typeof(SimpleMessageA)); + return b; + }); + if (messageBus == null) + return; - try + try + { + var countdown = new AsyncCountdownEvent(1); + await messageBus.SubscribeAsync(msg => { - var countdown = new AsyncCountdownEvent(numConcurrentMessages); - - int messages = 0; - await messageBus.SubscribeAsync(msg => - { - if (++messages % 50 == 0) - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Total Processed {Messages} messages", messages); - - Assert.Equal("Hello", msg.Data); - countdown.Signal(); - }); - - var sw = Stopwatch.StartNew(); - await Run.InParallelAsync(numConcurrentMessages, async i => - { - await messageBus.PublishAsync(new SimpleMessageA - { - Data = "Hello", - Count = i - }, new MessageOptions { DeliveryDelay = TimeSpan.FromMilliseconds(RandomData.GetInt(0, 100)) }); - if (i % 500 == 0) - _logger.LogTrace("Published 500 messages..."); - }); - - await countdown.WaitAsync(TimeSpan.FromSeconds(30)); - sw.Stop(); + _logger.LogTrace("Got message"); + Assert.Equal("Hello", msg.Data); + countdown.Signal(); + _logger.LogTrace("Set event"); + }); - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Processed {Processed} in {Duration:g}", numConcurrentMessages - countdown.CurrentCount, sw.Elapsed); - Assert.Equal(0, countdown.CurrentCount); - Assert.InRange(sw.Elapsed.TotalMilliseconds, 50, 30000); - } - finally + await SystemClock.SleepAsync(100); + await messageBus.PublishAsync(new SimpleMessageA { - await CleanupMessageBusAsync(messageBus); - } + Data = "Hello" + }); + _logger.LogTrace("Published one..."); + await countdown.WaitAsync(TimeSpan.FromSeconds(5)); + Assert.Equal(0, countdown.CurrentCount); } - - public virtual async Task CanSubscribeConcurrentlyAsync() + finally { - const int iterations = 100; - var messageBus = GetMessageBus(); - if (messageBus == null) - return; + await CleanupMessageBusAsync(messageBus); + } + } - try - { - var countdown = new AsyncCountdownEvent(iterations * 10); - await Run.InParallelAsync(10, i => - { - return messageBus.SubscribeAsync(msg => - { - Assert.Equal("Hello", msg.Data); - countdown.Signal(); - }); - }); + public virtual async Task CanSendDelayedMessageAsync() + { + const int numConcurrentMessages = 1000; + var messageBus = GetMessageBus(); + if (messageBus == null) + return; - await Run.InParallelAsync(iterations, i => messageBus.PublishAsync(new SimpleMessageA { Data = "Hello" })); - await countdown.WaitAsync(TimeSpan.FromSeconds(2)); - Assert.Equal(0, countdown.CurrentCount); - } - finally + try + { + var countdown = new AsyncCountdownEvent(numConcurrentMessages); + + int messages = 0; + await messageBus.SubscribeAsync(msg => { - await CleanupMessageBusAsync(messageBus); - } - } + if (++messages % 50 == 0) + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Total Processed {Messages} messages", messages); - public virtual async Task CanReceiveMessagesConcurrentlyAsync() - { - const int iterations = 100; - var messageBus = GetMessageBus(); - if (messageBus == null) - return; + Assert.Equal("Hello", msg.Data); + countdown.Signal(); + }); - var messageBuses = new List(10); - try + var sw = Stopwatch.StartNew(); + await Run.InParallelAsync(numConcurrentMessages, async i => { - var countdown = new AsyncCountdownEvent(iterations * 10); - await Run.InParallelAsync(10, async i => - { - var bus = GetMessageBus(); - await bus.SubscribeAsync(msg => - { - Assert.Equal("Hello", msg.Data); - countdown.Signal(); - }); - - messageBuses.Add(bus); - }); - var subscribe = Run.InParallelAsync(iterations, - i => - { - SystemClock.Sleep(RandomData.GetInt(0, 10)); - return messageBuses.Random().SubscribeAsync(msg => Task.CompletedTask); - }); - - var publish = Run.InParallelAsync(iterations + 3, i => + await messageBus.PublishAsync(new SimpleMessageA { - return i switch - { - 1 => messageBus.PublishAsync(new DerivedSimpleMessageA { Data = "Hello" }), - 2 => messageBus.PublishAsync(new Derived2SimpleMessageA { Data = "Hello" }), - 3 => messageBus.PublishAsync(new Derived3SimpleMessageA { Data = "Hello" }), - 4 => messageBus.PublishAsync(new Derived4SimpleMessageA { Data = "Hello" }), - 5 => messageBus.PublishAsync(new Derived5SimpleMessageA { Data = "Hello" }), - 6 => messageBus.PublishAsync(new Derived6SimpleMessageA { Data = "Hello" }), - 7 => messageBus.PublishAsync(new Derived7SimpleMessageA { Data = "Hello" }), - 8 => messageBus.PublishAsync(new Derived8SimpleMessageA { Data = "Hello" }), - 9 => messageBus.PublishAsync(new Derived9SimpleMessageA { Data = "Hello" }), - 10 => messageBus.PublishAsync(new Derived10SimpleMessageA { Data = "Hello" }), - iterations + 1 => messageBus.PublishAsync(new { Data = "Hello" }), - iterations + 2 => messageBus.PublishAsync(new SimpleMessageC { Data = "Hello" }), - iterations + 3 => messageBus.PublishAsync(new SimpleMessageB { Data = "Hello" }), - _ => messageBus.PublishAsync(new SimpleMessageA { Data = "Hello" }), - }; - }); + Data = "Hello", + Count = i + }, new MessageOptions { DeliveryDelay = TimeSpan.FromMilliseconds(RandomData.GetInt(0, 100)) }); + if (i % 500 == 0) + _logger.LogTrace("Published 500 messages..."); + }); - await Task.WhenAll(subscribe, publish); - await countdown.WaitAsync(TimeSpan.FromSeconds(2)); - Assert.Equal(0, countdown.CurrentCount); - } - finally - { - foreach (var mb in messageBuses) - await CleanupMessageBusAsync(mb); + await countdown.WaitAsync(TimeSpan.FromSeconds(30)); + sw.Stop(); - await CleanupMessageBusAsync(messageBus); - } + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Processed {Processed} in {Duration:g}", numConcurrentMessages - countdown.CurrentCount, sw.Elapsed); + Assert.Equal(0, countdown.CurrentCount); + Assert.InRange(sw.Elapsed.TotalMilliseconds, 50, 30000); } - - public virtual async Task CanSendMessageToMultipleSubscribersAsync() + finally { - var messageBus = GetMessageBus(); - if (messageBus == null) - return; + await CleanupMessageBusAsync(messageBus); + } + } - try + public virtual async Task CanSubscribeConcurrentlyAsync() + { + const int iterations = 100; + var messageBus = GetMessageBus(); + if (messageBus == null) + return; + + try + { + var countdown = new AsyncCountdownEvent(iterations * 10); + await Run.InParallelAsync(10, i => { - var countdown = new AsyncCountdownEvent(3); - await messageBus.SubscribeAsync(msg => + return messageBus.SubscribeAsync(msg => { Assert.Equal("Hello", msg.Data); countdown.Signal(); }); - await messageBus.SubscribeAsync(msg => - { - Assert.Equal("Hello", msg.Data); - countdown.Signal(); - }); - await messageBus.SubscribeAsync(msg => - { - Assert.Equal("Hello", msg.Data); - countdown.Signal(); - }); - await messageBus.PublishAsync(new SimpleMessageA - { - Data = "Hello" - }); + }); - await countdown.WaitAsync(TimeSpan.FromSeconds(2)); - Assert.Equal(0, countdown.CurrentCount); - } - finally - { - await CleanupMessageBusAsync(messageBus); - } + await Run.InParallelAsync(iterations, i => messageBus.PublishAsync(new SimpleMessageA { Data = "Hello" })); + await countdown.WaitAsync(TimeSpan.FromSeconds(2)); + Assert.Equal(0, countdown.CurrentCount); } - - public virtual async Task CanTolerateSubscriberFailureAsync() + finally { - var messageBus = GetMessageBus(); - if (messageBus == null) - return; + await CleanupMessageBusAsync(messageBus); + } + } - try + public virtual async Task CanReceiveMessagesConcurrentlyAsync() + { + const int iterations = 100; + var messageBus = GetMessageBus(); + if (messageBus == null) + return; + + var messageBuses = new List(10); + try + { + var countdown = new AsyncCountdownEvent(iterations * 10); + await Run.InParallelAsync(10, async i => { - var countdown = new AsyncCountdownEvent(4); - await messageBus.SubscribeAsync(msg => - { - Assert.Equal("Hello", msg.Data); - countdown.Signal(); - }); - await messageBus.SubscribeAsync(msg => + var bus = GetMessageBus(); + await bus.SubscribeAsync(msg => { Assert.Equal("Hello", msg.Data); countdown.Signal(); }); - await messageBus.SubscribeAsync(msg => - { - throw new Exception(); - }); - await messageBus.SubscribeAsync(msg => - { - Assert.Equal("Hello", msg.Data); - countdown.Signal(); - }); - await messageBus.SubscribeAsync(msg => - { - Assert.Equal("Hello", msg.Data); - countdown.Signal(); - }); - await messageBus.PublishAsync(new SimpleMessageA - { - Data = "Hello" - }); - await countdown.WaitAsync(TimeSpan.FromSeconds(2)); - Assert.Equal(0, countdown.CurrentCount); - } - finally - { - await CleanupMessageBusAsync(messageBus); - } - } + messageBuses.Add(bus); + }); + var subscribe = Run.InParallelAsync(iterations, + i => + { + SystemClock.Sleep(RandomData.GetInt(0, 10)); + return messageBuses.Random().SubscribeAsync(msg => Task.CompletedTask); + }); + + var publish = Run.InParallelAsync(iterations + 3, i => + { + return i switch + { + 1 => messageBus.PublishAsync(new DerivedSimpleMessageA { Data = "Hello" }), + 2 => messageBus.PublishAsync(new Derived2SimpleMessageA { Data = "Hello" }), + 3 => messageBus.PublishAsync(new Derived3SimpleMessageA { Data = "Hello" }), + 4 => messageBus.PublishAsync(new Derived4SimpleMessageA { Data = "Hello" }), + 5 => messageBus.PublishAsync(new Derived5SimpleMessageA { Data = "Hello" }), + 6 => messageBus.PublishAsync(new Derived6SimpleMessageA { Data = "Hello" }), + 7 => messageBus.PublishAsync(new Derived7SimpleMessageA { Data = "Hello" }), + 8 => messageBus.PublishAsync(new Derived8SimpleMessageA { Data = "Hello" }), + 9 => messageBus.PublishAsync(new Derived9SimpleMessageA { Data = "Hello" }), + 10 => messageBus.PublishAsync(new Derived10SimpleMessageA { Data = "Hello" }), + iterations + 1 => messageBus.PublishAsync(new { Data = "Hello" }), + iterations + 2 => messageBus.PublishAsync(new SimpleMessageC { Data = "Hello" }), + iterations + 3 => messageBus.PublishAsync(new SimpleMessageB { Data = "Hello" }), + _ => messageBus.PublishAsync(new SimpleMessageA { Data = "Hello" }), + }; + }); - public virtual async Task WillOnlyReceiveSubscribedMessageTypeAsync() + await Task.WhenAll(subscribe, publish); + await countdown.WaitAsync(TimeSpan.FromSeconds(2)); + Assert.Equal(0, countdown.CurrentCount); + } + finally { - var messageBus = GetMessageBus(); - if (messageBus == null) - return; + foreach (var mb in messageBuses) + await CleanupMessageBusAsync(mb); - try - { - var countdown = new AsyncCountdownEvent(1); - await messageBus.SubscribeAsync(msg => - { - Assert.Fail("Received wrong message type"); - }); - await messageBus.SubscribeAsync(msg => - { - Assert.Equal("Hello", msg.Data); - countdown.Signal(); - }); - await messageBus.PublishAsync(new SimpleMessageA - { - Data = "Hello" - }); + await CleanupMessageBusAsync(messageBus); + } + } - await countdown.WaitAsync(TimeSpan.FromSeconds(2)); - Assert.Equal(0, countdown.CurrentCount); - } - finally + public virtual async Task CanSendMessageToMultipleSubscribersAsync() + { + var messageBus = GetMessageBus(); + if (messageBus == null) + return; + + try + { + var countdown = new AsyncCountdownEvent(3); + await messageBus.SubscribeAsync(msg => { - await CleanupMessageBusAsync(messageBus); - } - } + Assert.Equal("Hello", msg.Data); + countdown.Signal(); + }); + await messageBus.SubscribeAsync(msg => + { + Assert.Equal("Hello", msg.Data); + countdown.Signal(); + }); + await messageBus.SubscribeAsync(msg => + { + Assert.Equal("Hello", msg.Data); + countdown.Signal(); + }); + await messageBus.PublishAsync(new SimpleMessageA + { + Data = "Hello" + }); - public virtual async Task WillReceiveDerivedMessageTypesAsync() + await countdown.WaitAsync(TimeSpan.FromSeconds(2)); + Assert.Equal(0, countdown.CurrentCount); + } + finally { - var messageBus = GetMessageBus(); - if (messageBus == null) - return; + await CleanupMessageBusAsync(messageBus); + } + } - try - { - var countdown = new AsyncCountdownEvent(2); - await messageBus.SubscribeAsync(msg => - { - Assert.Equal("Hello", msg.Data); - countdown.Signal(); - }); - await messageBus.PublishAsync(new SimpleMessageA - { - Data = "Hello" - }); - await messageBus.PublishAsync(new SimpleMessageB - { - Data = "Hello" - }); - await messageBus.PublishAsync(new SimpleMessageC - { - Data = "Hello" - }); + public virtual async Task CanTolerateSubscriberFailureAsync() + { + var messageBus = GetMessageBus(); + if (messageBus == null) + return; - await countdown.WaitAsync(TimeSpan.FromSeconds(5)); - Assert.Equal(0, countdown.CurrentCount); - } - finally + try + { + var countdown = new AsyncCountdownEvent(4); + await messageBus.SubscribeAsync(msg => { - await CleanupMessageBusAsync(messageBus); - } - } + Assert.Equal("Hello", msg.Data); + countdown.Signal(); + }); + await messageBus.SubscribeAsync(msg => + { + Assert.Equal("Hello", msg.Data); + countdown.Signal(); + }); + await messageBus.SubscribeAsync(msg => + { + throw new Exception(); + }); + await messageBus.SubscribeAsync(msg => + { + Assert.Equal("Hello", msg.Data); + countdown.Signal(); + }); + await messageBus.SubscribeAsync(msg => + { + Assert.Equal("Hello", msg.Data); + countdown.Signal(); + }); + await messageBus.PublishAsync(new SimpleMessageA + { + Data = "Hello" + }); - public virtual async Task CanSubscribeToRawMessagesAsync() + await countdown.WaitAsync(TimeSpan.FromSeconds(2)); + Assert.Equal(0, countdown.CurrentCount); + } + finally { - var messageBus = GetMessageBus(); - if (messageBus == null) - return; + await CleanupMessageBusAsync(messageBus); + } + } - try - { - var countdown = new AsyncCountdownEvent(3); - await messageBus.SubscribeAsync(msg => - { - Assert.True(msg.Type.Contains(nameof(SimpleMessageA)) - || msg.Type.Contains(nameof(SimpleMessageB)) - || msg.Type.Contains(nameof(SimpleMessageC))); - countdown.Signal(); - }); - await messageBus.PublishAsync(new SimpleMessageA - { - Data = "Hello" - }); - await messageBus.PublishAsync(new SimpleMessageB - { - Data = "Hello" - }); - await messageBus.PublishAsync(new SimpleMessageC - { - Data = "Hello" - }); + public virtual async Task WillOnlyReceiveSubscribedMessageTypeAsync() + { + var messageBus = GetMessageBus(); + if (messageBus == null) + return; - await countdown.WaitAsync(TimeSpan.FromSeconds(5)); - Assert.Equal(0, countdown.CurrentCount); - } - finally + try + { + var countdown = new AsyncCountdownEvent(1); + await messageBus.SubscribeAsync(msg => { - await CleanupMessageBusAsync(messageBus); - } - } + Assert.Fail("Received wrong message type"); + }); + await messageBus.SubscribeAsync(msg => + { + Assert.Equal("Hello", msg.Data); + countdown.Signal(); + }); + await messageBus.PublishAsync(new SimpleMessageA + { + Data = "Hello" + }); - public virtual async Task CanSubscribeToAllMessageTypesAsync() + await countdown.WaitAsync(TimeSpan.FromSeconds(2)); + Assert.Equal(0, countdown.CurrentCount); + } + finally { - var messageBus = GetMessageBus(); - if (messageBus == null) - return; + await CleanupMessageBusAsync(messageBus); + } + } - try - { - var countdown = new AsyncCountdownEvent(3); - await messageBus.SubscribeAsync(msg => - { - countdown.Signal(); - }); - await messageBus.PublishAsync(new SimpleMessageA - { - Data = "Hello" - }); - await messageBus.PublishAsync(new SimpleMessageB - { - Data = "Hello" - }); - await messageBus.PublishAsync(new SimpleMessageC - { - Data = "Hello" - }); + public virtual async Task WillReceiveDerivedMessageTypesAsync() + { + var messageBus = GetMessageBus(); + if (messageBus == null) + return; - await countdown.WaitAsync(TimeSpan.FromSeconds(2)); - Assert.Equal(0, countdown.CurrentCount); - } - finally + try + { + var countdown = new AsyncCountdownEvent(2); + await messageBus.SubscribeAsync(msg => { - await CleanupMessageBusAsync(messageBus); - } - } + Assert.Equal("Hello", msg.Data); + countdown.Signal(); + }); + await messageBus.PublishAsync(new SimpleMessageA + { + Data = "Hello" + }); + await messageBus.PublishAsync(new SimpleMessageB + { + Data = "Hello" + }); + await messageBus.PublishAsync(new SimpleMessageC + { + Data = "Hello" + }); - public virtual async Task WontKeepMessagesWithNoSubscribersAsync() + await countdown.WaitAsync(TimeSpan.FromSeconds(5)); + Assert.Equal(0, countdown.CurrentCount); + } + finally { - var messageBus = GetMessageBus(); - if (messageBus == null) - return; + await CleanupMessageBusAsync(messageBus); + } + } - try + public virtual async Task CanSubscribeToRawMessagesAsync() + { + var messageBus = GetMessageBus(); + if (messageBus == null) + return; + + try + { + var countdown = new AsyncCountdownEvent(3); + await messageBus.SubscribeAsync(msg => { - var countdown = new AsyncCountdownEvent(1); - await messageBus.PublishAsync(new SimpleMessageA - { - Data = "Hello" - }); + Assert.True(msg.Type.Contains(nameof(SimpleMessageA)) + || msg.Type.Contains(nameof(SimpleMessageB)) + || msg.Type.Contains(nameof(SimpleMessageC))); + countdown.Signal(); + }); + await messageBus.PublishAsync(new SimpleMessageA + { + Data = "Hello" + }); + await messageBus.PublishAsync(new SimpleMessageB + { + Data = "Hello" + }); + await messageBus.PublishAsync(new SimpleMessageC + { + Data = "Hello" + }); - await SystemClock.SleepAsync(100); - await messageBus.SubscribeAsync(msg => - { - Assert.Equal("Hello", msg.Data); - countdown.Signal(); - }); + await countdown.WaitAsync(TimeSpan.FromSeconds(5)); + Assert.Equal(0, countdown.CurrentCount); + } + finally + { + await CleanupMessageBusAsync(messageBus); + } + } - await countdown.WaitAsync(TimeSpan.FromMilliseconds(100)); - Assert.Equal(1, countdown.CurrentCount); - } - finally + public virtual async Task CanSubscribeToAllMessageTypesAsync() + { + var messageBus = GetMessageBus(); + if (messageBus == null) + return; + + try + { + var countdown = new AsyncCountdownEvent(3); + await messageBus.SubscribeAsync(msg => { - await CleanupMessageBusAsync(messageBus); - } + countdown.Signal(); + }); + await messageBus.PublishAsync(new SimpleMessageA + { + Data = "Hello" + }); + await messageBus.PublishAsync(new SimpleMessageB + { + Data = "Hello" + }); + await messageBus.PublishAsync(new SimpleMessageC + { + Data = "Hello" + }); + + await countdown.WaitAsync(TimeSpan.FromSeconds(2)); + Assert.Equal(0, countdown.CurrentCount); } + finally + { + await CleanupMessageBusAsync(messageBus); + } + } + + public virtual async Task WontKeepMessagesWithNoSubscribersAsync() + { + var messageBus = GetMessageBus(); + if (messageBus == null) + return; - public virtual async Task CanCancelSubscriptionAsync() + try { - var messageBus = GetMessageBus(); - if (messageBus == null) - return; + var countdown = new AsyncCountdownEvent(1); + await messageBus.PublishAsync(new SimpleMessageA + { + Data = "Hello" + }); - try + await SystemClock.SleepAsync(100); + await messageBus.SubscribeAsync(msg => { - var countdown = new AsyncCountdownEvent(2); + Assert.Equal("Hello", msg.Data); + countdown.Signal(); + }); - long messageCount = 0; - var cancellationTokenSource = new CancellationTokenSource(); - await messageBus.SubscribeAsync(async msg => - { - _logger.LogTrace("SimpleAMessage received"); - Interlocked.Increment(ref messageCount); - await cancellationTokenSource.CancelAsync(); - countdown.Signal(); - }, cancellationTokenSource.Token); + await countdown.WaitAsync(TimeSpan.FromMilliseconds(100)); + Assert.Equal(1, countdown.CurrentCount); + } + finally + { + await CleanupMessageBusAsync(messageBus); + } + } - await messageBus.SubscribeAsync(msg => countdown.Signal()); + public virtual async Task CanCancelSubscriptionAsync() + { + var messageBus = GetMessageBus(); + if (messageBus == null) + return; - await messageBus.PublishAsync(new SimpleMessageA - { - Data = "Hello" - }); + try + { + var countdown = new AsyncCountdownEvent(2); - await countdown.WaitAsync(TimeSpan.FromSeconds(2)); - Assert.Equal(0, countdown.CurrentCount); - Assert.Equal(1, messageCount); + long messageCount = 0; + var cancellationTokenSource = new CancellationTokenSource(); + await messageBus.SubscribeAsync(async msg => + { + _logger.LogTrace("SimpleAMessage received"); + Interlocked.Increment(ref messageCount); + await cancellationTokenSource.CancelAsync(); + countdown.Signal(); + }, cancellationTokenSource.Token); - countdown = new AsyncCountdownEvent(1); - await messageBus.PublishAsync(new SimpleMessageA - { - Data = "Hello" - }); + await messageBus.SubscribeAsync(msg => countdown.Signal()); - await countdown.WaitAsync(TimeSpan.FromSeconds(2)); - Assert.Equal(0, countdown.CurrentCount); - Assert.Equal(1, messageCount); - } - finally + await messageBus.PublishAsync(new SimpleMessageA { - await CleanupMessageBusAsync(messageBus); - } + Data = "Hello" + }); + + await countdown.WaitAsync(TimeSpan.FromSeconds(2)); + Assert.Equal(0, countdown.CurrentCount); + Assert.Equal(1, messageCount); + + countdown = new AsyncCountdownEvent(1); + await messageBus.PublishAsync(new SimpleMessageA + { + Data = "Hello" + }); + + await countdown.WaitAsync(TimeSpan.FromSeconds(2)); + Assert.Equal(0, countdown.CurrentCount); + Assert.Equal(1, messageCount); } + finally + { + await CleanupMessageBusAsync(messageBus); + } + } - public virtual async Task CanReceiveFromMultipleSubscribersAsync() + public virtual async Task CanReceiveFromMultipleSubscribersAsync() + { + var messageBus1 = GetMessageBus(); + if (messageBus1 == null) + return; + + try { - var messageBus1 = GetMessageBus(); - if (messageBus1 == null) - return; + var countdown1 = new AsyncCountdownEvent(1); + await messageBus1.SubscribeAsync(msg => + { + Assert.Equal("Hello", msg.Data); + countdown1.Signal(); + }); + var messageBus2 = GetMessageBus(); try { - var countdown1 = new AsyncCountdownEvent(1); - await messageBus1.SubscribeAsync(msg => + var countdown2 = new AsyncCountdownEvent(1); + await messageBus2.SubscribeAsync(msg => { Assert.Equal("Hello", msg.Data); - countdown1.Signal(); + countdown2.Signal(); }); - var messageBus2 = GetMessageBus(); - try - { - var countdown2 = new AsyncCountdownEvent(1); - await messageBus2.SubscribeAsync(msg => - { - Assert.Equal("Hello", msg.Data); - countdown2.Signal(); - }); - - await messageBus1.PublishAsync(new SimpleMessageA - { - Data = "Hello" - }); - - await countdown1.WaitAsync(TimeSpan.FromSeconds(20)); - Assert.Equal(0, countdown1.CurrentCount); - await countdown2.WaitAsync(TimeSpan.FromSeconds(20)); - Assert.Equal(0, countdown2.CurrentCount); - } - finally + await messageBus1.PublishAsync(new SimpleMessageA { - await CleanupMessageBusAsync(messageBus2); - } + Data = "Hello" + }); + + await countdown1.WaitAsync(TimeSpan.FromSeconds(20)); + Assert.Equal(0, countdown1.CurrentCount); + await countdown2.WaitAsync(TimeSpan.FromSeconds(20)); + Assert.Equal(0, countdown2.CurrentCount); } finally { - await CleanupMessageBusAsync(messageBus1); + await CleanupMessageBusAsync(messageBus2); } } - - public virtual void CanDisposeWithNoSubscribersOrPublishers() + finally { - var messageBus = GetMessageBus(); - if (messageBus == null) - return; - - using (messageBus) { } + await CleanupMessageBusAsync(messageBus1); } } + + public virtual void CanDisposeWithNoSubscribersOrPublishers() + { + var messageBus = GetMessageBus(); + if (messageBus == null) + return; + + using (messageBus) { } + } } diff --git a/src/Foundatio.TestHarness/Metrics/DiagnosticsMetricsCollector.cs b/src/Foundatio.TestHarness/Metrics/DiagnosticsMetricsCollector.cs index de1fc20b4..ce4f05aea 100644 --- a/src/Foundatio.TestHarness/Metrics/DiagnosticsMetricsCollector.cs +++ b/src/Foundatio.TestHarness/Metrics/DiagnosticsMetricsCollector.cs @@ -11,362 +11,361 @@ using Foundatio.Utility; using Microsoft.Extensions.Logging; -namespace Foundatio.Tests.Metrics +namespace Foundatio.Tests.Metrics; + +public class DiagnosticsMetricsCollector : IDisposable { - public class DiagnosticsMetricsCollector : IDisposable + private readonly MeterListener _meterListener = new(); + private readonly ConcurrentQueue> _byteMeasurements = new(); + private readonly ConcurrentQueue> _shortMeasurements = new(); + private readonly ConcurrentQueue> _intMeasurements = new(); + private readonly ConcurrentQueue> _longMeasurements = new(); + private readonly ConcurrentQueue> _floatMeasurements = new(); + private readonly ConcurrentQueue> _doubleMeasurements = new(); + private readonly ConcurrentQueue> _decimalMeasurements = new(); + private readonly int _maxMeasurementCountPerType = 1000; + private readonly AsyncAutoResetEvent _measurementEvent = new(false); + private readonly ILogger _logger; + + public DiagnosticsMetricsCollector(string metricNameOrPrefix, ILogger logger, int maxMeasurementCountPerType = 1000) : this(n => n.StartsWith(metricNameOrPrefix), logger, maxMeasurementCountPerType) { } + + public DiagnosticsMetricsCollector(Func shouldCollect, ILogger logger, int maxMeasurementCount = 1000) { - private readonly MeterListener _meterListener = new(); - private readonly ConcurrentQueue> _byteMeasurements = new(); - private readonly ConcurrentQueue> _shortMeasurements = new(); - private readonly ConcurrentQueue> _intMeasurements = new(); - private readonly ConcurrentQueue> _longMeasurements = new(); - private readonly ConcurrentQueue> _floatMeasurements = new(); - private readonly ConcurrentQueue> _doubleMeasurements = new(); - private readonly ConcurrentQueue> _decimalMeasurements = new(); - private readonly int _maxMeasurementCountPerType = 1000; - private readonly AsyncAutoResetEvent _measurementEvent = new(false); - private readonly ILogger _logger; - - public DiagnosticsMetricsCollector(string metricNameOrPrefix, ILogger logger, int maxMeasurementCountPerType = 1000) : this(n => n.StartsWith(metricNameOrPrefix), logger, maxMeasurementCountPerType) { } - - public DiagnosticsMetricsCollector(Func shouldCollect, ILogger logger, int maxMeasurementCount = 1000) - { - _logger = logger; - _maxMeasurementCountPerType = maxMeasurementCount; - - _meterListener.InstrumentPublished = (instrument, listener) => - { - if (shouldCollect(instrument.Meter.Name)) - listener.EnableMeasurementEvents(instrument); - }; + _logger = logger; + _maxMeasurementCountPerType = maxMeasurementCount; - _meterListener.SetMeasurementEventCallback((instrument, measurement, tags, state) => - { - _byteMeasurements.Enqueue(new RecordedMeasurement(instrument, measurement, ref tags, state)); - if (_byteMeasurements.Count > _maxMeasurementCountPerType) - _byteMeasurements.TryDequeue(out _); - _measurementEvent.Set(); - }); + _meterListener.InstrumentPublished = (instrument, listener) => + { + if (shouldCollect(instrument.Meter.Name)) + listener.EnableMeasurementEvents(instrument); + }; - _meterListener.SetMeasurementEventCallback((instrument, measurement, tags, state) => - { - _shortMeasurements.Enqueue(new RecordedMeasurement(instrument, measurement, ref tags, state)); - if (_shortMeasurements.Count > _maxMeasurementCountPerType) - _shortMeasurements.TryDequeue(out _); - _measurementEvent.Set(); - }); + _meterListener.SetMeasurementEventCallback((instrument, measurement, tags, state) => + { + _byteMeasurements.Enqueue(new RecordedMeasurement(instrument, measurement, ref tags, state)); + if (_byteMeasurements.Count > _maxMeasurementCountPerType) + _byteMeasurements.TryDequeue(out _); + _measurementEvent.Set(); + }); - _meterListener.SetMeasurementEventCallback((instrument, measurement, tags, state) => - { - _intMeasurements.Enqueue(new RecordedMeasurement(instrument, measurement, ref tags, state)); - if (_intMeasurements.Count > _maxMeasurementCountPerType) - _intMeasurements.TryDequeue(out _); - _measurementEvent.Set(); - }); + _meterListener.SetMeasurementEventCallback((instrument, measurement, tags, state) => + { + _shortMeasurements.Enqueue(new RecordedMeasurement(instrument, measurement, ref tags, state)); + if (_shortMeasurements.Count > _maxMeasurementCountPerType) + _shortMeasurements.TryDequeue(out _); + _measurementEvent.Set(); + }); - _meterListener.SetMeasurementEventCallback((instrument, measurement, tags, state) => - { - _longMeasurements.Enqueue(new RecordedMeasurement(instrument, measurement, ref tags, state)); - if (_longMeasurements.Count > _maxMeasurementCountPerType) - _longMeasurements.TryDequeue(out _); - _measurementEvent.Set(); - }); + _meterListener.SetMeasurementEventCallback((instrument, measurement, tags, state) => + { + _intMeasurements.Enqueue(new RecordedMeasurement(instrument, measurement, ref tags, state)); + if (_intMeasurements.Count > _maxMeasurementCountPerType) + _intMeasurements.TryDequeue(out _); + _measurementEvent.Set(); + }); - _meterListener.SetMeasurementEventCallback((instrument, measurement, tags, state) => - { - _floatMeasurements.Enqueue(new RecordedMeasurement(instrument, measurement, ref tags, state)); - if (_floatMeasurements.Count > _maxMeasurementCountPerType) - _floatMeasurements.TryDequeue(out _); - _measurementEvent.Set(); - }); + _meterListener.SetMeasurementEventCallback((instrument, measurement, tags, state) => + { + _longMeasurements.Enqueue(new RecordedMeasurement(instrument, measurement, ref tags, state)); + if (_longMeasurements.Count > _maxMeasurementCountPerType) + _longMeasurements.TryDequeue(out _); + _measurementEvent.Set(); + }); - _meterListener.SetMeasurementEventCallback((instrument, measurement, tags, state) => - { - _doubleMeasurements.Enqueue(new RecordedMeasurement(instrument, measurement, ref tags, state)); - if (_doubleMeasurements.Count > _maxMeasurementCountPerType) - _doubleMeasurements.TryDequeue(out _); - _measurementEvent.Set(); - }); + _meterListener.SetMeasurementEventCallback((instrument, measurement, tags, state) => + { + _floatMeasurements.Enqueue(new RecordedMeasurement(instrument, measurement, ref tags, state)); + if (_floatMeasurements.Count > _maxMeasurementCountPerType) + _floatMeasurements.TryDequeue(out _); + _measurementEvent.Set(); + }); - _meterListener.SetMeasurementEventCallback((instrument, measurement, tags, state) => - { - _decimalMeasurements.Enqueue(new RecordedMeasurement(instrument, measurement, ref tags, state)); - if (_decimalMeasurements.Count > _maxMeasurementCountPerType) - _decimalMeasurements.TryDequeue(out _); - _measurementEvent.Set(); - }); + _meterListener.SetMeasurementEventCallback((instrument, measurement, tags, state) => + { + _doubleMeasurements.Enqueue(new RecordedMeasurement(instrument, measurement, ref tags, state)); + if (_doubleMeasurements.Count > _maxMeasurementCountPerType) + _doubleMeasurements.TryDequeue(out _); + _measurementEvent.Set(); + }); - _meterListener.Start(); - } + _meterListener.SetMeasurementEventCallback((instrument, measurement, tags, state) => + { + _decimalMeasurements.Enqueue(new RecordedMeasurement(instrument, measurement, ref tags, state)); + if (_decimalMeasurements.Count > _maxMeasurementCountPerType) + _decimalMeasurements.TryDequeue(out _); + _measurementEvent.Set(); + }); + + _meterListener.Start(); + } + + public void RecordObservableInstruments() + { + _meterListener.RecordObservableInstruments(); + } - public void RecordObservableInstruments() + public IReadOnlyCollection> GetMeasurements(string name = null) where T : struct + { + if (typeof(T) == typeof(byte)) { - _meterListener.RecordObservableInstruments(); + if (name == null) + return ImmutableList.CreateRange((IEnumerable>)_byteMeasurements); + else + return ImmutableList.CreateRange(((IEnumerable>)_byteMeasurements).Where(m => m.Name == name)); } - - public IReadOnlyCollection> GetMeasurements(string name = null) where T : struct + else if (typeof(T) == typeof(short)) { - if (typeof(T) == typeof(byte)) - { - if (name == null) - return ImmutableList.CreateRange((IEnumerable>)_byteMeasurements); - else - return ImmutableList.CreateRange(((IEnumerable>)_byteMeasurements).Where(m => m.Name == name)); - } - else if (typeof(T) == typeof(short)) - { - if (name == null) - return ImmutableList.CreateRange((IEnumerable>)_shortMeasurements); - else - return ImmutableList.CreateRange(((IEnumerable>)_shortMeasurements).Where(m => m.Name == name)); - } - else if (typeof(T) == typeof(int)) - { - if (name == null) - return ImmutableList.CreateRange((IEnumerable>)_intMeasurements); - else - return ImmutableList.CreateRange(((IEnumerable>)_intMeasurements).Where(m => m.Name == name)); - } - else if (typeof(T) == typeof(long)) - { - if (name == null) - return ImmutableList.CreateRange((IEnumerable>)_longMeasurements); - else - return ImmutableList.CreateRange(((IEnumerable>)_longMeasurements).Where(m => m.Name == name)); - } - else if (typeof(T) == typeof(float)) - { - if (name == null) - return ImmutableList.CreateRange((IEnumerable>)_floatMeasurements); - else - return ImmutableList.CreateRange(((IEnumerable>)_floatMeasurements).Where(m => m.Name == name)); - } - else if (typeof(T) == typeof(double)) - { - if (name == null) - return ImmutableList.CreateRange((IEnumerable>)_doubleMeasurements); - else - return ImmutableList.CreateRange(((IEnumerable>)_doubleMeasurements).Where(m => m.Name == name)); - } - else if (typeof(T) == typeof(decimal)) - { - if (name == null) - return ImmutableList.CreateRange((IEnumerable>)_decimalMeasurements); - else - return ImmutableList.CreateRange(((IEnumerable>)_decimalMeasurements).Where(m => m.Name == name)); - } + if (name == null) + return ImmutableList.CreateRange((IEnumerable>)_shortMeasurements); else - { - return ImmutableList.Create>(); - } - - // byte, short, int, long, float, double, decimal + return ImmutableList.CreateRange(((IEnumerable>)_shortMeasurements).Where(m => m.Name == name)); } - - public int GetCount(string name) where T : struct + else if (typeof(T) == typeof(int)) { - return GetMeasurements().Count(m => m.Name == name); + if (name == null) + return ImmutableList.CreateRange((IEnumerable>)_intMeasurements); + else + return ImmutableList.CreateRange(((IEnumerable>)_intMeasurements).Where(m => m.Name == name)); } - - public double GetSum(string name) where T : struct + else if (typeof(T) == typeof(long)) { - if (typeof(T) == typeof(byte)) - { - var measurements = GetMeasurements(name); - return measurements.Sum(m => m.Value); - } - else if (typeof(T) == typeof(short)) - { - var measurements = GetMeasurements(name); - return measurements.Sum(m => m.Value); - } - else if (typeof(T) == typeof(int)) - { - var measurements = GetMeasurements(name); - return measurements.Sum(m => m.Value); - } - else if (typeof(T) == typeof(long)) - { - var measurements = GetMeasurements(name); - return measurements.Sum(m => m.Value); - } - else if (typeof(T) == typeof(float)) - { - var measurements = GetMeasurements(name); - return measurements.Sum(m => m.Value); - } - else if (typeof(T) == typeof(double)) - { - var measurements = GetMeasurements(name); - return measurements.Sum(m => m.Value); - } - else if (typeof(T) == typeof(decimal)) - { - var measurements = GetMeasurements(name); - return measurements.Sum(m => (double)m.Value); - } + if (name == null) + return ImmutableList.CreateRange((IEnumerable>)_longMeasurements); else - { - return 0; - } + return ImmutableList.CreateRange(((IEnumerable>)_longMeasurements).Where(m => m.Name == name)); } - - public double GetAvg(string name) where T : struct + else if (typeof(T) == typeof(float)) { - if (typeof(T) == typeof(byte)) - { - var measurements = GetMeasurements(name); - return measurements.Average(m => m.Value); - } - else if (typeof(T) == typeof(short)) - { - var measurements = GetMeasurements(name); - return measurements.Average(m => m.Value); - } - else if (typeof(T) == typeof(int)) - { - var measurements = GetMeasurements(name); - return measurements.Average(m => m.Value); - } - else if (typeof(T) == typeof(long)) - { - var measurements = GetMeasurements(name); - return measurements.Average(m => m.Value); - } - else if (typeof(T) == typeof(float)) - { - var measurements = GetMeasurements(name); - return measurements.Average(m => m.Value); - } - else if (typeof(T) == typeof(double)) - { - var measurements = GetMeasurements(name); - return measurements.Average(m => m.Value); - } - else if (typeof(T) == typeof(decimal)) - { - var measurements = GetMeasurements(name); - return measurements.Average(m => (double)m.Value); - } + if (name == null) + return ImmutableList.CreateRange((IEnumerable>)_floatMeasurements); else - { - return 0; - } + return ImmutableList.CreateRange(((IEnumerable>)_floatMeasurements).Where(m => m.Name == name)); } - - public double GetMax(string name) where T : struct + else if (typeof(T) == typeof(double)) { - if (typeof(T) == typeof(byte)) - { - var measurements = GetMeasurements(name); - return measurements.Max(m => m.Value); - } - else if (typeof(T) == typeof(short)) - { - var measurements = GetMeasurements(name); - return measurements.Max(m => m.Value); - } - else if (typeof(T) == typeof(int)) - { - var measurements = GetMeasurements(name); - return measurements.Max(m => m.Value); - } - else if (typeof(T) == typeof(long)) - { - var measurements = GetMeasurements(name); - return measurements.Max(m => m.Value); - } - else if (typeof(T) == typeof(float)) - { - var measurements = GetMeasurements(name); - return measurements.Max(m => m.Value); - } - else if (typeof(T) == typeof(double)) - { - var measurements = GetMeasurements(name); - return measurements.Max(m => m.Value); - } - else if (typeof(T) == typeof(decimal)) - { - var measurements = GetMeasurements(name); - return measurements.Max(m => (double)m.Value); - } + if (name == null) + return ImmutableList.CreateRange((IEnumerable>)_doubleMeasurements); else - { - return 0; - } + return ImmutableList.CreateRange(((IEnumerable>)_doubleMeasurements).Where(m => m.Name == name)); + } + else if (typeof(T) == typeof(decimal)) + { + if (name == null) + return ImmutableList.CreateRange((IEnumerable>)_decimalMeasurements); + else + return ImmutableList.CreateRange(((IEnumerable>)_decimalMeasurements).Where(m => m.Name == name)); + } + else + { + return ImmutableList.Create>(); } - public async Task WaitForCounterAsync(string statName, long count = 1, TimeSpan? timeout = null) where T : struct + // byte, short, int, long, float, double, decimal + } + + public int GetCount(string name) where T : struct + { + return GetMeasurements().Count(m => m.Name == name); + } + + public double GetSum(string name) where T : struct + { + if (typeof(T) == typeof(byte)) + { + var measurements = GetMeasurements(name); + return measurements.Sum(m => m.Value); + } + else if (typeof(T) == typeof(short)) { - using var cancellationTokenSource = timeout.ToCancellationTokenSource(TimeSpan.FromMinutes(1)); - return await WaitForCounterAsync(statName, () => Task.CompletedTask, count, cancellationTokenSource.Token).AnyContext(); + var measurements = GetMeasurements(name); + return measurements.Sum(m => m.Value); } + else if (typeof(T) == typeof(int)) + { + var measurements = GetMeasurements(name); + return measurements.Sum(m => m.Value); + } + else if (typeof(T) == typeof(long)) + { + var measurements = GetMeasurements(name); + return measurements.Sum(m => m.Value); + } + else if (typeof(T) == typeof(float)) + { + var measurements = GetMeasurements(name); + return measurements.Sum(m => m.Value); + } + else if (typeof(T) == typeof(double)) + { + var measurements = GetMeasurements(name); + return measurements.Sum(m => m.Value); + } + else if (typeof(T) == typeof(decimal)) + { + var measurements = GetMeasurements(name); + return measurements.Sum(m => (double)m.Value); + } + else + { + return 0; + } + } - public async Task WaitForCounterAsync(string name, Func work, long count = 1, CancellationToken cancellationToken = default) where T : struct + public double GetAvg(string name) where T : struct + { + if (typeof(T) == typeof(byte)) + { + var measurements = GetMeasurements(name); + return measurements.Average(m => m.Value); + } + else if (typeof(T) == typeof(short)) + { + var measurements = GetMeasurements(name); + return measurements.Average(m => m.Value); + } + else if (typeof(T) == typeof(int)) + { + var measurements = GetMeasurements(name); + return measurements.Average(m => m.Value); + } + else if (typeof(T) == typeof(long)) + { + var measurements = GetMeasurements(name); + return measurements.Average(m => m.Value); + } + else if (typeof(T) == typeof(float)) + { + var measurements = GetMeasurements(name); + return measurements.Average(m => m.Value); + } + else if (typeof(T) == typeof(double)) + { + var measurements = GetMeasurements(name); + return measurements.Average(m => m.Value); + } + else if (typeof(T) == typeof(decimal)) + { + var measurements = GetMeasurements(name); + return measurements.Average(m => (double)m.Value); + } + else { - if (count <= 0) - return true; + return 0; + } + } - if (cancellationToken == default) - { - using var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromMinutes(1)); - cancellationToken = cancellationTokenSource.Token; - } + public double GetMax(string name) where T : struct + { + if (typeof(T) == typeof(byte)) + { + var measurements = GetMeasurements(name); + return measurements.Max(m => m.Value); + } + else if (typeof(T) == typeof(short)) + { + var measurements = GetMeasurements(name); + return measurements.Max(m => m.Value); + } + else if (typeof(T) == typeof(int)) + { + var measurements = GetMeasurements(name); + return measurements.Max(m => m.Value); + } + else if (typeof(T) == typeof(long)) + { + var measurements = GetMeasurements(name); + return measurements.Max(m => m.Value); + } + else if (typeof(T) == typeof(float)) + { + var measurements = GetMeasurements(name); + return measurements.Max(m => m.Value); + } + else if (typeof(T) == typeof(double)) + { + var measurements = GetMeasurements(name); + return measurements.Max(m => m.Value); + } + else if (typeof(T) == typeof(decimal)) + { + var measurements = GetMeasurements(name); + return measurements.Max(m => (double)m.Value); + } + else + { + return 0; + } + } - var start = SystemClock.UtcNow; + public async Task WaitForCounterAsync(string statName, long count = 1, TimeSpan? timeout = null) where T : struct + { + using var cancellationTokenSource = timeout.ToCancellationTokenSource(TimeSpan.FromMinutes(1)); + return await WaitForCounterAsync(statName, () => Task.CompletedTask, count, cancellationTokenSource.Token).AnyContext(); + } - var currentCount = (int)GetSum(name); - var targetCount = currentCount + count; + public async Task WaitForCounterAsync(string name, Func work, long count = 1, CancellationToken cancellationToken = default) where T : struct + { + if (count <= 0) + return true; - if (work != null) - await work().AnyContext(); + if (cancellationToken == default) + { + using var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromMinutes(1)); + cancellationToken = cancellationTokenSource.Token; + } - _logger.LogTrace("Wait: count={Count}", count); - currentCount = (int)GetSum(name); + var start = SystemClock.UtcNow; - while (!cancellationToken.IsCancellationRequested && currentCount < targetCount) - { - try - { - await _measurementEvent.WaitAsync(cancellationToken); - } - catch (OperationCanceledException) { } - currentCount = (int)GetSum(name); - _logger.LogTrace("Got new measurement: count={CurrentCount} expected={Count}", currentCount, targetCount); - } + var currentCount = (int)GetSum(name); + var targetCount = currentCount + count; - _logger.LogTrace("Done waiting: count={CurrentCount} expected={Count} success={Success} time={Time}", currentCount, targetCount, currentCount >= targetCount, SystemClock.UtcNow.Subtract(start)); + if (work != null) + await work().AnyContext(); - return currentCount >= targetCount; - } + _logger.LogTrace("Wait: count={Count}", count); + currentCount = (int)GetSum(name); - public void Dispose() + while (!cancellationToken.IsCancellationRequested && currentCount < targetCount) { - GC.SuppressFinalize(this); - _meterListener?.Dispose(); + try + { + await _measurementEvent.WaitAsync(cancellationToken); + } + catch (OperationCanceledException) { } + currentCount = (int)GetSum(name); + _logger.LogTrace("Got new measurement: count={CurrentCount} expected={Count}", currentCount, targetCount); } + + _logger.LogTrace("Done waiting: count={CurrentCount} expected={Count} success={Success} time={Time}", currentCount, targetCount, currentCount >= targetCount, SystemClock.UtcNow.Subtract(start)); + + return currentCount >= targetCount; } - [DebuggerDisplay("{Name}={Value}")] - public struct RecordedMeasurement where T : struct + public void Dispose() { - public RecordedMeasurement(Instrument instrument, T value, ref ReadOnlySpan> tags, object state) - { - Instrument = instrument; - Name = Instrument.Name; - Value = value; - if (tags.Length > 0) - Tags = ImmutableDictionary.CreateRange(tags.ToArray()); - else - Tags = ImmutableDictionary.Empty; - State = state; - } + GC.SuppressFinalize(this); + _meterListener?.Dispose(); + } +} - public Instrument Instrument { get; } - public string Name { get; } - public T Value { get; } - public IReadOnlyDictionary Tags { get; } - public object State { get; } +[DebuggerDisplay("{Name}={Value}")] +public struct RecordedMeasurement where T : struct +{ + public RecordedMeasurement(Instrument instrument, T value, ref ReadOnlySpan> tags, object state) + { + Instrument = instrument; + Name = Instrument.Name; + Value = value; + if (tags.Length > 0) + Tags = ImmutableDictionary.CreateRange(tags.ToArray()); + else + Tags = ImmutableDictionary.Empty; + State = state; } + + public Instrument Instrument { get; } + public string Name { get; } + public T Value { get; } + public IReadOnlyDictionary Tags { get; } + public object State { get; } } diff --git a/src/Foundatio.TestHarness/Metrics/MetricsClientTestBase.cs b/src/Foundatio.TestHarness/Metrics/MetricsClientTestBase.cs index ca0454a13..dd8cde1ed 100644 --- a/src/Foundatio.TestHarness/Metrics/MetricsClientTestBase.cs +++ b/src/Foundatio.TestHarness/Metrics/MetricsClientTestBase.cs @@ -12,207 +12,206 @@ using Xunit.Abstractions; #pragma warning disable AsyncFixer04 // A disposable object used in a fire & forget async call -namespace Foundatio.Tests.Metrics +namespace Foundatio.Tests.Metrics; + +public abstract class MetricsClientTestBase : TestWithLoggingBase { - public abstract class MetricsClientTestBase : TestWithLoggingBase - { - public MetricsClientTestBase(ITestOutputHelper output) : base(output) { } + public MetricsClientTestBase(ITestOutputHelper output) : base(output) { } - public abstract IMetricsClient GetMetricsClient(bool buffered = false); + public abstract IMetricsClient GetMetricsClient(bool buffered = false); - public virtual async Task CanSetGaugesAsync() - { - using var metrics = GetMetricsClient(); - - if (metrics is not IMetricsClientStats stats) - return; - - metrics.Gauge("mygauge", 12d); - Assert.Equal(12d, (await stats.GetGaugeStatsAsync("mygauge")).Last); - metrics.Gauge("mygauge", 10d); - metrics.Gauge("mygauge", 5d); - metrics.Gauge("mygauge", 4d); - metrics.Gauge("mygauge", 12d); - metrics.Gauge("mygauge", 20d); - Assert.Equal(20d, (await stats.GetGaugeStatsAsync("mygauge")).Last); - } + public virtual async Task CanSetGaugesAsync() + { + using var metrics = GetMetricsClient(); + + if (metrics is not IMetricsClientStats stats) + return; + + metrics.Gauge("mygauge", 12d); + Assert.Equal(12d, (await stats.GetGaugeStatsAsync("mygauge")).Last); + metrics.Gauge("mygauge", 10d); + metrics.Gauge("mygauge", 5d); + metrics.Gauge("mygauge", 4d); + metrics.Gauge("mygauge", 12d); + metrics.Gauge("mygauge", 20d); + Assert.Equal(20d, (await stats.GetGaugeStatsAsync("mygauge")).Last); + } - public virtual async Task CanIncrementCounterAsync() - { - using var metrics = GetMetricsClient(); + public virtual async Task CanIncrementCounterAsync() + { + using var metrics = GetMetricsClient(); - if (metrics is not IMetricsClientStats stats) - return; + if (metrics is not IMetricsClientStats stats) + return; - metrics.Counter("c1"); - await AssertCounterAsync(stats, "c1", 1); + metrics.Counter("c1"); + await AssertCounterAsync(stats, "c1", 1); - metrics.Counter("c1", 5); - await AssertCounterAsync(stats, "c1", 6); + metrics.Counter("c1", 5); + await AssertCounterAsync(stats, "c1", 6); - metrics.Gauge("g1", 2.534); - Assert.Equal(2.534, await stats.GetLastGaugeValueAsync("g1")); + metrics.Gauge("g1", 2.534); + Assert.Equal(2.534, await stats.GetLastGaugeValueAsync("g1")); - metrics.Timer("t1", 50788); - var timer = await stats.GetTimerStatsAsync("t1"); - Assert.Equal(1, timer.Count); + metrics.Timer("t1", 50788); + var timer = await stats.GetTimerStatsAsync("t1"); + Assert.Equal(1, timer.Count); - if (_logger.IsEnabled(LogLevel.Information)) - _logger.LogInformation((await stats.GetCounterStatsAsync("c1")).ToString()); - } + if (_logger.IsEnabled(LogLevel.Information)) + _logger.LogInformation((await stats.GetCounterStatsAsync("c1")).ToString()); + } - private Task AssertCounterAsync(IMetricsClientStats client, string name, long expected) + private Task AssertCounterAsync(IMetricsClientStats client, string name, long expected) + { + return Run.WithRetriesAsync(async () => { - return Run.WithRetriesAsync(async () => - { - long actual = await client.GetCounterCountAsync(name, SystemClock.UtcNow.Subtract(TimeSpan.FromHours(1))); - Assert.Equal(expected, actual); - }, 8, logger: _logger); - } + long actual = await client.GetCounterCountAsync(name, SystemClock.UtcNow.Subtract(TimeSpan.FromHours(1))); + Assert.Equal(expected, actual); + }, 8, logger: _logger); + } - public virtual async Task CanGetBufferedQueueMetricsAsync() - { - using var metrics = GetMetricsClient(true) as IBufferedMetricsClient; - - if (metrics is not IMetricsClientStats stats) - return; - - using var behavior = new MetricsQueueBehavior(metrics, reportCountsInterval: TimeSpan.FromMilliseconds(25), loggerFactory: Log); - using var queue = new InMemoryQueue(new InMemoryQueueOptions { Behaviors = new[] { behavior }, LoggerFactory = Log }); - - await queue.EnqueueAsync(new SimpleWorkItem { Id = 1, Data = "1" }); - await SystemClock.SleepAsync(50); - var entry = await queue.DequeueAsync(TimeSpan.Zero); - await SystemClock.SleepAsync(15); - await entry.CompleteAsync(); - - await SystemClock.SleepAsync(100); // give queue metrics time - await metrics.FlushAsync(); - var queueStats = await stats.GetQueueStatsAsync("simpleworkitem"); - Assert.Equal(1, queueStats.Count.Max); - Assert.Equal(0, queueStats.Count.Last); - Assert.Equal(1, queueStats.Enqueued.Count); - Assert.Equal(1, queueStats.Dequeued.Count); - Assert.Equal(1, queueStats.Completed.Count); - Assert.InRange(queueStats.QueueTime.AverageDuration, 45, 250); - Assert.InRange(queueStats.ProcessTime.AverageDuration, 10, 250); - } + public virtual async Task CanGetBufferedQueueMetricsAsync() + { + using var metrics = GetMetricsClient(true) as IBufferedMetricsClient; + + if (metrics is not IMetricsClientStats stats) + return; + + using var behavior = new MetricsQueueBehavior(metrics, reportCountsInterval: TimeSpan.FromMilliseconds(25), loggerFactory: Log); + using var queue = new InMemoryQueue(new InMemoryQueueOptions { Behaviors = new[] { behavior }, LoggerFactory = Log }); + + await queue.EnqueueAsync(new SimpleWorkItem { Id = 1, Data = "1" }); + await SystemClock.SleepAsync(50); + var entry = await queue.DequeueAsync(TimeSpan.Zero); + await SystemClock.SleepAsync(15); + await entry.CompleteAsync(); + + await SystemClock.SleepAsync(100); // give queue metrics time + await metrics.FlushAsync(); + var queueStats = await stats.GetQueueStatsAsync("simpleworkitem"); + Assert.Equal(1, queueStats.Count.Max); + Assert.Equal(0, queueStats.Count.Last); + Assert.Equal(1, queueStats.Enqueued.Count); + Assert.Equal(1, queueStats.Dequeued.Count); + Assert.Equal(1, queueStats.Completed.Count); + Assert.InRange(queueStats.QueueTime.AverageDuration, 45, 250); + Assert.InRange(queueStats.ProcessTime.AverageDuration, 10, 250); + } - public virtual async Task CanIncrementBufferedCounterAsync() - { - using var metrics = GetMetricsClient(true) as IBufferedMetricsClient; - - if (metrics is not IMetricsClientStats stats) - return; - - metrics.Counter("c1"); - await metrics.FlushAsync(); - var counter = await stats.GetCounterStatsAsync("c1", SystemClock.UtcNow.AddMinutes(-5), SystemClock.UtcNow); - Assert.Equal(1, counter.Count); - - metrics.Counter("c1", 5); - await metrics.FlushAsync(); - counter = await stats.GetCounterStatsAsync("c1", SystemClock.UtcNow.AddMinutes(-5), SystemClock.UtcNow); - Assert.Equal(6, counter.Count); - - metrics.Gauge("g1", 5.34); - await metrics.FlushAsync(); - var gauge = await stats.GetGaugeStatsAsync("g1", SystemClock.UtcNow.AddMinutes(-5), SystemClock.UtcNow); - Assert.Equal(5.34, gauge.Last); - Assert.Equal(5.34, gauge.Max); - - metrics.Gauge("g1", 2.534); - await metrics.FlushAsync(); - gauge = await stats.GetGaugeStatsAsync("g1", SystemClock.UtcNow.AddMinutes(-5), SystemClock.UtcNow); - Assert.Equal(2.534, gauge.Last); - Assert.Equal(5.34, gauge.Max); - - metrics.Timer("t1", 50788); - await metrics.FlushAsync(); - var timer = await stats.GetTimerStatsAsync("t1", SystemClock.UtcNow.AddMinutes(-5), SystemClock.UtcNow); - Assert.Equal(1, timer.Count); - Assert.Equal(50788, timer.TotalDuration); - - metrics.Timer("t1", 98); - metrics.Timer("t1", 102); - await metrics.FlushAsync(); - timer = await stats.GetTimerStatsAsync("t1", SystemClock.UtcNow.AddMinutes(-5), SystemClock.UtcNow); - Assert.Equal(3, timer.Count); - Assert.Equal(50788 + 98 + 102, timer.TotalDuration); - } + public virtual async Task CanIncrementBufferedCounterAsync() + { + using var metrics = GetMetricsClient(true) as IBufferedMetricsClient; + + if (metrics is not IMetricsClientStats stats) + return; + + metrics.Counter("c1"); + await metrics.FlushAsync(); + var counter = await stats.GetCounterStatsAsync("c1", SystemClock.UtcNow.AddMinutes(-5), SystemClock.UtcNow); + Assert.Equal(1, counter.Count); + + metrics.Counter("c1", 5); + await metrics.FlushAsync(); + counter = await stats.GetCounterStatsAsync("c1", SystemClock.UtcNow.AddMinutes(-5), SystemClock.UtcNow); + Assert.Equal(6, counter.Count); + + metrics.Gauge("g1", 5.34); + await metrics.FlushAsync(); + var gauge = await stats.GetGaugeStatsAsync("g1", SystemClock.UtcNow.AddMinutes(-5), SystemClock.UtcNow); + Assert.Equal(5.34, gauge.Last); + Assert.Equal(5.34, gauge.Max); + + metrics.Gauge("g1", 2.534); + await metrics.FlushAsync(); + gauge = await stats.GetGaugeStatsAsync("g1", SystemClock.UtcNow.AddMinutes(-5), SystemClock.UtcNow); + Assert.Equal(2.534, gauge.Last); + Assert.Equal(5.34, gauge.Max); + + metrics.Timer("t1", 50788); + await metrics.FlushAsync(); + var timer = await stats.GetTimerStatsAsync("t1", SystemClock.UtcNow.AddMinutes(-5), SystemClock.UtcNow); + Assert.Equal(1, timer.Count); + Assert.Equal(50788, timer.TotalDuration); + + metrics.Timer("t1", 98); + metrics.Timer("t1", 102); + await metrics.FlushAsync(); + timer = await stats.GetTimerStatsAsync("t1", SystemClock.UtcNow.AddMinutes(-5), SystemClock.UtcNow); + Assert.Equal(3, timer.Count); + Assert.Equal(50788 + 98 + 102, timer.TotalDuration); + } #pragma warning disable 4014 - public virtual async Task CanWaitForCounterAsync() - { - const string CounterName = "Test"; - using var metrics = GetMetricsClient() as CacheBucketMetricsClientBase; + public virtual async Task CanWaitForCounterAsync() + { + const string CounterName = "Test"; + using var metrics = GetMetricsClient() as CacheBucketMetricsClientBase; - if (metrics is not IMetricsClientStats stats) - return; + if (metrics is not IMetricsClientStats stats) + return; - Task.Run(async () => - { - await SystemClock.SleepAsync(TimeSpan.FromMilliseconds(50)); - metrics.Counter(CounterName); - }); + Task.Run(async () => + { + await SystemClock.SleepAsync(TimeSpan.FromMilliseconds(50)); + metrics.Counter(CounterName); + }); - var sw = Stopwatch.StartNew(); - var task = metrics.WaitForCounterAsync(CounterName, 1, TimeSpan.FromMilliseconds(500)); - await SystemClock.SleepAsync(TimeSpan.FromMilliseconds(100)); - Assert.True(await task, $"Expected at least 1 count within 500 ms... Took: {sw.Elapsed:g}"); + var sw = Stopwatch.StartNew(); + var task = metrics.WaitForCounterAsync(CounterName, 1, TimeSpan.FromMilliseconds(500)); + await SystemClock.SleepAsync(TimeSpan.FromMilliseconds(100)); + Assert.True(await task, $"Expected at least 1 count within 500 ms... Took: {sw.Elapsed:g}"); - Task.Run(async () => - { - await SystemClock.SleepAsync(TimeSpan.FromMilliseconds(50)); - metrics.Counter(CounterName); - }); + Task.Run(async () => + { + await SystemClock.SleepAsync(TimeSpan.FromMilliseconds(50)); + metrics.Counter(CounterName); + }); - sw.Restart(); - task = metrics.WaitForCounterAsync(CounterName, timeout: TimeSpan.FromMilliseconds(500)); - await SystemClock.SleepAsync(TimeSpan.FromMilliseconds(100)); - Assert.True(await task, $"Expected at least 2 count within 500 ms... Took: {sw.Elapsed:g}"); + sw.Restart(); + task = metrics.WaitForCounterAsync(CounterName, timeout: TimeSpan.FromMilliseconds(500)); + await SystemClock.SleepAsync(TimeSpan.FromMilliseconds(100)); + Assert.True(await task, $"Expected at least 2 count within 500 ms... Took: {sw.Elapsed:g}"); - Task.Run(async () => - { - await SystemClock.SleepAsync(TimeSpan.FromMilliseconds(50)); - metrics.Counter(CounterName, 2); - }); + Task.Run(async () => + { + await SystemClock.SleepAsync(TimeSpan.FromMilliseconds(50)); + metrics.Counter(CounterName, 2); + }); - sw.Restart(); - task = metrics.WaitForCounterAsync(CounterName, 2, TimeSpan.FromMilliseconds(500)); - await SystemClock.SleepAsync(TimeSpan.FromMilliseconds(100)); - Assert.True(await task, $"Expected at least 4 count within 500 ms... Took: {sw.Elapsed:g}"); + sw.Restart(); + task = metrics.WaitForCounterAsync(CounterName, 2, TimeSpan.FromMilliseconds(500)); + await SystemClock.SleepAsync(TimeSpan.FromMilliseconds(100)); + Assert.True(await task, $"Expected at least 4 count within 500 ms... Took: {sw.Elapsed:g}"); - using (var timeoutCancellationTokenSource = new CancellationTokenSource(500)) + using (var timeoutCancellationTokenSource = new CancellationTokenSource(500)) + { + sw.Restart(); + task = metrics.WaitForCounterAsync(CounterName, () => { - sw.Restart(); - task = metrics.WaitForCounterAsync(CounterName, () => - { - metrics.Counter(CounterName); - return Task.CompletedTask; - }, cancellationToken: timeoutCancellationTokenSource.Token); - await SystemClock.SleepAsync(TimeSpan.FromMilliseconds(500)); - Assert.True(await task, $"Expected at least 5 count within 500 ms... Took: {sw.Elapsed:g}"); - } - - if (_logger.IsEnabled(LogLevel.Information)) - _logger.LogInformation((await metrics.GetCounterStatsAsync(CounterName)).ToString()); + metrics.Counter(CounterName); + return Task.CompletedTask; + }, cancellationToken: timeoutCancellationTokenSource.Token); + await SystemClock.SleepAsync(TimeSpan.FromMilliseconds(500)); + Assert.True(await task, $"Expected at least 5 count within 500 ms... Took: {sw.Elapsed:g}"); } + + if (_logger.IsEnabled(LogLevel.Information)) + _logger.LogInformation((await metrics.GetCounterStatsAsync(CounterName)).ToString()); + } #pragma warning restore 4014 - public virtual async Task CanSendBufferedMetricsAsync() - { - using var metrics = GetMetricsClient(true) as IBufferedMetricsClient; + public virtual async Task CanSendBufferedMetricsAsync() + { + using var metrics = GetMetricsClient(true) as IBufferedMetricsClient; - if (metrics is not IMetricsClientStats stats) - return; + if (metrics is not IMetricsClientStats stats) + return; - Parallel.For(0, 100, i => metrics.Counter("c1")); + Parallel.For(0, 100, i => metrics.Counter("c1")); - await metrics.FlushAsync(); + await metrics.FlushAsync(); - var counter = await stats.GetCounterStatsAsync("c1"); - Assert.Equal(100, counter.Count); - } + var counter = await stats.GetCounterStatsAsync("c1"); + Assert.Equal(100, counter.Count); } } diff --git a/src/Foundatio.TestHarness/Queue/QueueTestBase.cs b/src/Foundatio.TestHarness/Queue/QueueTestBase.cs index 98f97d0ce..1bf7a243d 100644 --- a/src/Foundatio.TestHarness/Queue/QueueTestBase.cs +++ b/src/Foundatio.TestHarness/Queue/QueueTestBase.cs @@ -21,1633 +21,1632 @@ using Xunit.Abstractions; #pragma warning disable CS4014 -namespace Foundatio.Tests.Queue +namespace Foundatio.Tests.Queue; + +public abstract class QueueTestBase : TestWithLoggingBase, IDisposable { - public abstract class QueueTestBase : TestWithLoggingBase, IDisposable + protected QueueTestBase(ITestOutputHelper output) : base(output) + { + Log.SetLogLevel(LogLevel.Debug); + Log.SetLogLevel(LogLevel.Debug); + Log.SetLogLevel>(LogLevel.Debug); + Log.SetLogLevel(LogLevel.Debug); + } + + protected virtual IQueue GetQueue(int retries = 1, TimeSpan? workItemTimeout = null, TimeSpan? retryDelay = null, int[] retryMultipliers = null, int deadLetterMaxItems = 100, bool runQueueMaintenance = true) { - protected QueueTestBase(ITestOutputHelper output) : base(output) + return null; + } + + protected virtual async Task CleanupQueueAsync(IQueue queue) + { + if (queue == null) + return; + + try { - Log.SetLogLevel(LogLevel.Debug); - Log.SetLogLevel(LogLevel.Debug); - Log.SetLogLevel>(LogLevel.Debug); - Log.SetLogLevel(LogLevel.Debug); + await queue.DeleteQueueAsync(); } - - protected virtual IQueue GetQueue(int retries = 1, TimeSpan? workItemTimeout = null, TimeSpan? retryDelay = null, int[] retryMultipliers = null, int deadLetterMaxItems = 100, bool runQueueMaintenance = true) + catch (Exception ex) { - return null; + if (_logger.IsEnabled(LogLevel.Error)) + _logger.LogError(ex, "Error cleaning up queue"); } - - protected virtual async Task CleanupQueueAsync(IQueue queue) + finally { - if (queue == null) - return; - - try - { - await queue.DeleteQueueAsync(); - } - catch (Exception ex) - { - if (_logger.IsEnabled(LogLevel.Error)) - _logger.LogError(ex, "Error cleaning up queue"); - } - finally - { - queue.Dispose(); - } + queue.Dispose(); } + } - protected bool _assertStats = true; - - public virtual async Task CanQueueAndDequeueWorkItemAsync() - { - var queue = GetQueue(); - if (queue == null) - return; + protected bool _assertStats = true; - using var metricsCollector = new DiagnosticsMetricsCollector(FoundatioDiagnostics.Meter.Name, _logger); + public virtual async Task CanQueueAndDequeueWorkItemAsync() + { + var queue = GetQueue(); + if (queue == null) + return; - try - { - await queue.DeleteQueueAsync(); - await AssertEmptyQueueAsync(queue); + using var metricsCollector = new DiagnosticsMetricsCollector(FoundatioDiagnostics.Meter.Name, _logger); - await queue.EnqueueAsync(new SimpleWorkItem - { - Data = "Hello", - SubMetricName = "myitem" - }); - Assert.Equal(1, (await queue.GetQueueStatsAsync()).Enqueued); + try + { + await queue.DeleteQueueAsync(); + await AssertEmptyQueueAsync(queue); - var workItem = await queue.DequeueAsync(TimeSpan.Zero); - Assert.NotNull(workItem); - Assert.Equal("Hello", workItem.Value.Data); - if (_assertStats) - Assert.Equal(1, (await queue.GetQueueStatsAsync()).Dequeued); + await queue.EnqueueAsync(new SimpleWorkItem + { + Data = "Hello", + SubMetricName = "myitem" + }); + Assert.Equal(1, (await queue.GetQueueStatsAsync()).Enqueued); + + var workItem = await queue.DequeueAsync(TimeSpan.Zero); + Assert.NotNull(workItem); + Assert.Equal("Hello", workItem.Value.Data); + if (_assertStats) + Assert.Equal(1, (await queue.GetQueueStatsAsync()).Dequeued); - await workItem.CompleteAsync(); - Assert.False(workItem.IsAbandoned); - Assert.True(workItem.IsCompleted); + await workItem.CompleteAsync(); + Assert.False(workItem.IsAbandoned); + Assert.True(workItem.IsCompleted); - metricsCollector.RecordObservableInstruments(); - if (_assertStats) - { - var stats = await queue.GetQueueStatsAsync(); - Assert.Equal(1, stats.Completed); - Assert.Equal(0, stats.Queued); + metricsCollector.RecordObservableInstruments(); + if (_assertStats) + { + var stats = await queue.GetQueueStatsAsync(); + Assert.Equal(1, stats.Completed); + Assert.Equal(0, stats.Queued); - Assert.Equal(1, metricsCollector.GetSum("foundatio.simpleworkitem.enqueued")); - Assert.Equal(1, metricsCollector.GetSum("foundatio.simpleworkitem.dequeued")); - Assert.Equal(1, metricsCollector.GetSum("foundatio.simpleworkitem.completed")); + Assert.Equal(1, metricsCollector.GetSum("foundatio.simpleworkitem.enqueued")); + Assert.Equal(1, metricsCollector.GetSum("foundatio.simpleworkitem.dequeued")); + Assert.Equal(1, metricsCollector.GetSum("foundatio.simpleworkitem.completed")); - Assert.Equal(0, metricsCollector.GetSum("foundatio.simpleworkitem.count")); - Assert.Equal(0, metricsCollector.GetSum("foundatio.simpleworkitem.working")); - Assert.Equal(0, metricsCollector.GetSum("foundatio.simpleworkitem.deadletter")); + Assert.Equal(0, metricsCollector.GetSum("foundatio.simpleworkitem.count")); + Assert.Equal(0, metricsCollector.GetSum("foundatio.simpleworkitem.working")); + Assert.Equal(0, metricsCollector.GetSum("foundatio.simpleworkitem.deadletter")); - Assert.Equal(1, metricsCollector.GetSum("foundatio.simpleworkitem.myitem.enqueued")); - Assert.Equal(1, metricsCollector.GetSum("foundatio.simpleworkitem.myitem.dequeued")); - Assert.Equal(1, metricsCollector.GetSum("foundatio.simpleworkitem.myitem.completed")); - } - } - finally - { - await CleanupQueueAsync(queue); + Assert.Equal(1, metricsCollector.GetSum("foundatio.simpleworkitem.myitem.enqueued")); + Assert.Equal(1, metricsCollector.GetSum("foundatio.simpleworkitem.myitem.dequeued")); + Assert.Equal(1, metricsCollector.GetSum("foundatio.simpleworkitem.myitem.completed")); } } - - public virtual async Task CanQueueAndDequeueWorkItemWithDelayAsync() + finally { - var queue = GetQueue(); - if (queue == null) - return; - - using var metricsCollector = new DiagnosticsMetricsCollector(FoundatioDiagnostics.Meter.Name, _logger); + await CleanupQueueAsync(queue); + } + } - try - { - await queue.DeleteQueueAsync(); - await AssertEmptyQueueAsync(queue); + public virtual async Task CanQueueAndDequeueWorkItemWithDelayAsync() + { + var queue = GetQueue(); + if (queue == null) + return; - await queue.EnqueueAsync(new SimpleWorkItem - { - Data = "Hello" - }, new QueueEntryOptions { DeliveryDelay = TimeSpan.FromSeconds(1) }); - Assert.Equal(1, (await queue.GetQueueStatsAsync()).Enqueued); + using var metricsCollector = new DiagnosticsMetricsCollector(FoundatioDiagnostics.Meter.Name, _logger); - var workItem = await queue.DequeueAsync(TimeSpan.Zero); - Assert.Null(workItem); + try + { + await queue.DeleteQueueAsync(); + await AssertEmptyQueueAsync(queue); - workItem = await queue.DequeueAsync(TimeSpan.FromSeconds(2)); - Assert.NotNull(workItem); - Assert.Equal("Hello", workItem.Value.Data); - if (_assertStats) - Assert.Equal(1, (await queue.GetQueueStatsAsync()).Dequeued); + await queue.EnqueueAsync(new SimpleWorkItem + { + Data = "Hello" + }, new QueueEntryOptions { DeliveryDelay = TimeSpan.FromSeconds(1) }); + Assert.Equal(1, (await queue.GetQueueStatsAsync()).Enqueued); - await workItem.CompleteAsync(); - Assert.False(workItem.IsAbandoned); - Assert.True(workItem.IsCompleted); + var workItem = await queue.DequeueAsync(TimeSpan.Zero); + Assert.Null(workItem); - if (_assertStats) - { - var stats = await queue.GetQueueStatsAsync(); - Assert.Equal(1, stats.Completed); - Assert.Equal(0, stats.Queued); + workItem = await queue.DequeueAsync(TimeSpan.FromSeconds(2)); + Assert.NotNull(workItem); + Assert.Equal("Hello", workItem.Value.Data); + if (_assertStats) + Assert.Equal(1, (await queue.GetQueueStatsAsync()).Dequeued); - metricsCollector.RecordObservableInstruments(); - Assert.Equal(1, metricsCollector.GetSum("foundatio.simpleworkitem.enqueued")); - Assert.Equal(1, metricsCollector.GetSum("foundatio.simpleworkitem.dequeued")); - Assert.Equal(1, metricsCollector.GetSum("foundatio.simpleworkitem.completed")); + await workItem.CompleteAsync(); + Assert.False(workItem.IsAbandoned); + Assert.True(workItem.IsCompleted); - Assert.Equal(0, metricsCollector.GetSum("foundatio.simpleworkitem.count")); - Assert.Equal(0, metricsCollector.GetSum("foundatio.simpleworkitem.working")); - Assert.Equal(0, metricsCollector.GetSum("foundatio.simpleworkitem.deadletter")); - } - } - finally + if (_assertStats) { - await CleanupQueueAsync(queue); + var stats = await queue.GetQueueStatsAsync(); + Assert.Equal(1, stats.Completed); + Assert.Equal(0, stats.Queued); + + metricsCollector.RecordObservableInstruments(); + Assert.Equal(1, metricsCollector.GetSum("foundatio.simpleworkitem.enqueued")); + Assert.Equal(1, metricsCollector.GetSum("foundatio.simpleworkitem.dequeued")); + Assert.Equal(1, metricsCollector.GetSum("foundatio.simpleworkitem.completed")); + + Assert.Equal(0, metricsCollector.GetSum("foundatio.simpleworkitem.count")); + Assert.Equal(0, metricsCollector.GetSum("foundatio.simpleworkitem.working")); + Assert.Equal(0, metricsCollector.GetSum("foundatio.simpleworkitem.deadletter")); } } - - public virtual async Task CanUseQueueOptionsAsync() + finally { - var queue = GetQueue(retryDelay: TimeSpan.Zero); - if (queue == null) - return; - - using var metricsCollector = new DiagnosticsMetricsCollector(FoundatioDiagnostics.Meter.Name, _logger); - - try - { - using var listener = new ActivityListener - { - ShouldListenTo = s => s.Name == "Foundatio", - Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded, - ActivityStarted = activity => _logger.LogInformation("Start: " + activity.DisplayName), - ActivityStopped = activity => _logger.LogInformation("Stop: " + activity.DisplayName) - }; - - Activity.Current = new Activity("Parent"); + await CleanupQueueAsync(queue); + } + } - ActivitySource.AddActivityListener(listener); + public virtual async Task CanUseQueueOptionsAsync() + { + var queue = GetQueue(retryDelay: TimeSpan.Zero); + if (queue == null) + return; - await queue.DeleteQueueAsync(); - await AssertEmptyQueueAsync(queue); + using var metricsCollector = new DiagnosticsMetricsCollector(FoundatioDiagnostics.Meter.Name, _logger); - await queue.EnqueueAsync(new SimpleWorkItem - { - Data = "Hello" - }, new QueueEntryOptions - { - CorrelationId = "123+456", - Properties = new Dictionary { - { "hey", "now" } - } - }); - Assert.Equal(1, (await queue.GetQueueStatsAsync()).Enqueued); + try + { + using var listener = new ActivityListener + { + ShouldListenTo = s => s.Name == "Foundatio", + Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded, + ActivityStarted = activity => _logger.LogInformation("Start: " + activity.DisplayName), + ActivityStopped = activity => _logger.LogInformation("Stop: " + activity.DisplayName) + }; - var workItem = await queue.DequeueAsync(TimeSpan.Zero); - Assert.NotNull(workItem); - Assert.Equal("Hello", workItem.Value.Data); - Assert.Equal("123+456", workItem.CorrelationId); - Assert.Single(workItem.Properties); - Assert.Contains(workItem.Properties, i => i.Key == "hey" && i.Value.ToString() == "now"); - if (_assertStats) - { - Assert.Equal(1, (await queue.GetQueueStatsAsync()).Dequeued); - Assert.Equal(1, metricsCollector.GetSum("foundatio.simpleworkitem.dequeued")); - } + Activity.Current = new Activity("Parent"); - await workItem.AbandonAsync(); - Assert.True(workItem.IsAbandoned); - Assert.False(workItem.IsCompleted); - await Task.Delay(100); + ActivitySource.AddActivityListener(listener); - if (_assertStats) - { - var stats = await queue.GetQueueStatsAsync(); - Assert.Equal(1, stats.Abandoned); - Assert.Equal(0, stats.Completed); - Assert.Equal(1, stats.Queued); + await queue.DeleteQueueAsync(); + await AssertEmptyQueueAsync(queue); - metricsCollector.RecordObservableInstruments(); - Assert.Equal(0, metricsCollector.GetSum("foundatio.simpleworkitem.completed")); - Assert.Equal(1, metricsCollector.GetSum("foundatio.simpleworkitem.abandoned")); - Assert.Equal(1, metricsCollector.GetSum("foundatio.simpleworkitem.count")); + await queue.EnqueueAsync(new SimpleWorkItem + { + Data = "Hello" + }, new QueueEntryOptions + { + CorrelationId = "123+456", + Properties = new Dictionary { + { "hey", "now" } } - - workItem = await queue.DequeueAsync(TimeSpan.FromSeconds(10)); - Assert.NotNull(workItem); - Assert.Equal("Hello", workItem.Value.Data); - Assert.Equal("123+456", workItem.CorrelationId); - Assert.Equal(2, workItem.Attempts); - Assert.Single(workItem.Properties); - Assert.Contains(workItem.Properties, i => i.Key == "hey" && i.Value.ToString() == "now"); - } - finally + }); + Assert.Equal(1, (await queue.GetQueueStatsAsync()).Enqueued); + + var workItem = await queue.DequeueAsync(TimeSpan.Zero); + Assert.NotNull(workItem); + Assert.Equal("Hello", workItem.Value.Data); + Assert.Equal("123+456", workItem.CorrelationId); + Assert.Single(workItem.Properties); + Assert.Contains(workItem.Properties, i => i.Key == "hey" && i.Value.ToString() == "now"); + if (_assertStats) { - await CleanupQueueAsync(queue); + Assert.Equal(1, (await queue.GetQueueStatsAsync()).Dequeued); + Assert.Equal(1, metricsCollector.GetSum("foundatio.simpleworkitem.dequeued")); } - } - public virtual async Task CanDiscardDuplicateQueueEntriesAsync() - { - var queue = GetQueue(); - if (queue == null) - return; + await workItem.AbandonAsync(); + Assert.True(workItem.IsAbandoned); + Assert.False(workItem.IsCompleted); + await Task.Delay(100); - using var metricsCollector = new DiagnosticsMetricsCollector(FoundatioDiagnostics.Meter.Name, _logger); - - try + if (_assertStats) { - await queue.DeleteQueueAsync(); - await AssertEmptyQueueAsync(queue); - queue.AttachBehavior(new DuplicateDetectionQueueBehavior(new InMemoryCacheClient(), Log)); - - await queue.EnqueueAsync(new SimpleWorkItem - { - Data = "Hello", - UniqueIdentifier = "123" - }); - if (_assertStats) - { - Assert.Equal(1, (await queue.GetQueueStatsAsync()).Enqueued); - Assert.Equal(1, metricsCollector.GetSum("foundatio.simpleworkitem.enqueued")); - } - - await queue.EnqueueAsync(new SimpleWorkItem - { - Data = "Hello", - UniqueIdentifier = "123" - }); - if (_assertStats) - { - Assert.Equal(1, (await queue.GetQueueStatsAsync()).Enqueued); - Assert.Equal(1, metricsCollector.GetSum("foundatio.simpleworkitem.enqueued")); - } - - var workItem = await queue.DequeueAsync(TimeSpan.Zero); - Assert.NotNull(workItem); - Assert.Equal("Hello", workItem.Value.Data); - if (_assertStats) - { - Assert.Equal(1, (await queue.GetQueueStatsAsync()).Dequeued); - Assert.Equal(1, metricsCollector.GetSum("foundatio.simpleworkitem.dequeued")); - } - - await queue.EnqueueAsync(new SimpleWorkItem - { - Data = "Hello", - UniqueIdentifier = "123" - }); - if (_assertStats) - { - Assert.Equal(2, (await queue.GetQueueStatsAsync()).Enqueued); - Assert.Equal(2, metricsCollector.GetSum("foundatio.simpleworkitem.enqueued")); - } - - await workItem.CompleteAsync(); - Assert.False(workItem.IsAbandoned); - Assert.True(workItem.IsCompleted); var stats = await queue.GetQueueStatsAsync(); - if (_assertStats) - { - Assert.Equal(1, stats.Completed); - Assert.Equal(1, stats.Queued); - - metricsCollector.RecordObservableInstruments(); - Assert.Equal(2, metricsCollector.GetSum("foundatio.simpleworkitem.enqueued")); - Assert.Equal(1, metricsCollector.GetSum("foundatio.simpleworkitem.dequeued")); - Assert.Equal(1, metricsCollector.GetSum("foundatio.simpleworkitem.completed")); - Assert.Equal(0, metricsCollector.GetSum("foundatio.simpleworkitem.abandoned")); - - Assert.Equal(1, metricsCollector.GetSum("foundatio.simpleworkitem.count")); - Assert.Equal(0, metricsCollector.GetSum("foundatio.simpleworkitem.working")); - Assert.Equal(0, metricsCollector.GetSum("foundatio.simpleworkitem.deadletter")); - } + Assert.Equal(1, stats.Abandoned); + Assert.Equal(0, stats.Completed); + Assert.Equal(1, stats.Queued); + metricsCollector.RecordObservableInstruments(); + Assert.Equal(0, metricsCollector.GetSum("foundatio.simpleworkitem.completed")); + Assert.Equal(1, metricsCollector.GetSum("foundatio.simpleworkitem.abandoned")); + Assert.Equal(1, metricsCollector.GetSum("foundatio.simpleworkitem.count")); } - finally - { - await CleanupQueueAsync(queue); - } - } - public virtual Task VerifyRetryAttemptsAsync() + workItem = await queue.DequeueAsync(TimeSpan.FromSeconds(10)); + Assert.NotNull(workItem); + Assert.Equal("Hello", workItem.Value.Data); + Assert.Equal("123+456", workItem.CorrelationId); + Assert.Equal(2, workItem.Attempts); + Assert.Single(workItem.Properties); + Assert.Contains(workItem.Properties, i => i.Key == "hey" && i.Value.ToString() == "now"); + } + finally { - const int retryCount = 2; - var queue = GetQueue(retryCount, TimeSpan.FromSeconds(1), TimeSpan.Zero, new[] { 1 }); - if (queue == null) - return Task.CompletedTask; - - return VerifyRetryAttemptsImplAsync(queue, retryCount, TimeSpan.FromSeconds(10)); + await CleanupQueueAsync(queue); } + } - public virtual Task VerifyDelayedRetryAttemptsAsync() - { - const int retryCount = 2; - var queue = GetQueue(retryCount, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1), new[] { 1 }); - if (queue == null) - return Task.CompletedTask; + public virtual async Task CanDiscardDuplicateQueueEntriesAsync() + { + var queue = GetQueue(); + if (queue == null) + return; - return VerifyRetryAttemptsImplAsync(queue, retryCount, TimeSpan.FromSeconds(30)); - } + using var metricsCollector = new DiagnosticsMetricsCollector(FoundatioDiagnostics.Meter.Name, _logger); - private async Task VerifyRetryAttemptsImplAsync(IQueue queue, int retryCount, TimeSpan waitTime) + try { - try + await queue.DeleteQueueAsync(); + await AssertEmptyQueueAsync(queue); + queue.AttachBehavior(new DuplicateDetectionQueueBehavior(new InMemoryCacheClient(), Log)); + + await queue.EnqueueAsync(new SimpleWorkItem { - await queue.DeleteQueueAsync(); - await AssertEmptyQueueAsync(queue); + Data = "Hello", + UniqueIdentifier = "123" + }); + if (_assertStats) + { + Assert.Equal(1, (await queue.GetQueueStatsAsync()).Enqueued); + Assert.Equal(1, metricsCollector.GetSum("foundatio.simpleworkitem.enqueued")); + } - using var metricsCollector = new DiagnosticsMetricsCollector(FoundatioDiagnostics.Meter.Name, _logger); + await queue.EnqueueAsync(new SimpleWorkItem + { + Data = "Hello", + UniqueIdentifier = "123" + }); + if (_assertStats) + { + Assert.Equal(1, (await queue.GetQueueStatsAsync()).Enqueued); + Assert.Equal(1, metricsCollector.GetSum("foundatio.simpleworkitem.enqueued")); + } - var countdown = new AsyncCountdownEvent(retryCount + 1); - int attempts = 0; - await queue.StartWorkingAsync(async w => - { - Interlocked.Increment(ref attempts); - _logger.LogInformation("Starting Attempt {Attempt} to work on queue item", attempts); - Assert.Equal("Hello", w.Value.Data); + var workItem = await queue.DequeueAsync(TimeSpan.Zero); + Assert.NotNull(workItem); + Assert.Equal("Hello", workItem.Value.Data); + if (_assertStats) + { + Assert.Equal(1, (await queue.GetQueueStatsAsync()).Dequeued); + Assert.Equal(1, metricsCollector.GetSum("foundatio.simpleworkitem.dequeued")); + } - var queueEntryMetadata = (IQueueEntryMetadata)w; - Assert.Equal(attempts, queueEntryMetadata.Attempts); + await queue.EnqueueAsync(new SimpleWorkItem + { + Data = "Hello", + UniqueIdentifier = "123" + }); + if (_assertStats) + { + Assert.Equal(2, (await queue.GetQueueStatsAsync()).Enqueued); + Assert.Equal(2, metricsCollector.GetSum("foundatio.simpleworkitem.enqueued")); + } - await w.AbandonAsync(); - countdown.Signal(); + await workItem.CompleteAsync(); + Assert.False(workItem.IsAbandoned); + Assert.True(workItem.IsCompleted); + var stats = await queue.GetQueueStatsAsync(); + if (_assertStats) + { + Assert.Equal(1, stats.Completed); + Assert.Equal(1, stats.Queued); - _logger.LogInformation("Finished Attempt {Attempt} to work on queue item, Metadata Attempts: {MetadataAttempts}", attempts, queueEntryMetadata.Attempts); - }); + metricsCollector.RecordObservableInstruments(); + Assert.Equal(2, metricsCollector.GetSum("foundatio.simpleworkitem.enqueued")); + Assert.Equal(1, metricsCollector.GetSum("foundatio.simpleworkitem.dequeued")); + Assert.Equal(1, metricsCollector.GetSum("foundatio.simpleworkitem.completed")); + Assert.Equal(0, metricsCollector.GetSum("foundatio.simpleworkitem.abandoned")); + + Assert.Equal(1, metricsCollector.GetSum("foundatio.simpleworkitem.count")); + Assert.Equal(0, metricsCollector.GetSum("foundatio.simpleworkitem.working")); + Assert.Equal(0, metricsCollector.GetSum("foundatio.simpleworkitem.deadletter")); + } - await queue.EnqueueAsync(new SimpleWorkItem - { - Data = "Hello" - }); + } + finally + { + await CleanupQueueAsync(queue); + } + } - await countdown.WaitAsync(waitTime); - Assert.Equal(0, countdown.CurrentCount); + public virtual Task VerifyRetryAttemptsAsync() + { + const int retryCount = 2; + var queue = GetQueue(retryCount, TimeSpan.FromSeconds(1), TimeSpan.Zero, new[] { 1 }); + if (queue == null) + return Task.CompletedTask; - if (_assertStats) - { - var stats = await queue.GetQueueStatsAsync(); - Assert.Equal(retryCount + 1, attempts); - Assert.Equal(0, stats.Completed); - Assert.Equal(0, stats.Queued); - Assert.Equal(0, stats.Errors); - Assert.Equal(retryCount + 1, stats.Dequeued); - Assert.Equal(retryCount + 1, stats.Abandoned); + return VerifyRetryAttemptsImplAsync(queue, retryCount, TimeSpan.FromSeconds(10)); + } - metricsCollector.RecordObservableInstruments(); - Assert.Equal(retryCount + 1, metricsCollector.GetSum("foundatio.simpleworkitem.dequeued")); - Assert.Equal(0, metricsCollector.GetSum("foundatio.simpleworkitem.completed")); - Assert.Equal(retryCount + 1, metricsCollector.GetSum("foundatio.simpleworkitem.abandoned")); + public virtual Task VerifyDelayedRetryAttemptsAsync() + { + const int retryCount = 2; + var queue = GetQueue(retryCount, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1), new[] { 1 }); + if (queue == null) + return Task.CompletedTask; - Assert.Equal(0, metricsCollector.GetSum("foundatio.simpleworkitem.count")); - } - } - finally - { - await CleanupQueueAsync(queue); - } - } + return VerifyRetryAttemptsImplAsync(queue, retryCount, TimeSpan.FromSeconds(30)); + } - /// - /// When a cancelled token is passed into Dequeue, it will only try to dequeue one time and then exit. - /// - /// - public virtual async Task CanDequeueWithCancelledTokenAsync() + private async Task VerifyRetryAttemptsImplAsync(IQueue queue, int retryCount, TimeSpan waitTime) + { + try { - var queue = GetQueue(); - if (queue == null) - return; + await queue.DeleteQueueAsync(); + await AssertEmptyQueueAsync(queue); using var metricsCollector = new DiagnosticsMetricsCollector(FoundatioDiagnostics.Meter.Name, _logger); - try + var countdown = new AsyncCountdownEvent(retryCount + 1); + int attempts = 0; + await queue.StartWorkingAsync(async w => { - await queue.DeleteQueueAsync(); - await AssertEmptyQueueAsync(queue); + Interlocked.Increment(ref attempts); + _logger.LogInformation("Starting Attempt {Attempt} to work on queue item", attempts); + Assert.Equal("Hello", w.Value.Data); - await queue.EnqueueAsync(new SimpleWorkItem - { - Data = "Hello" - }); - if (_assertStats) - { - Assert.Equal(1, (await queue.GetQueueStatsAsync()).Enqueued); - Assert.Equal(1, metricsCollector.GetSum("foundatio.simpleworkitem.enqueued")); - } + var queueEntryMetadata = (IQueueEntryMetadata)w; + Assert.Equal(attempts, queueEntryMetadata.Attempts); - var workItem = await queue.DequeueAsync(new CancellationToken(true)); - Assert.NotNull(workItem); - Assert.Equal("Hello", workItem.Value.Data); - Assert.Equal(1, (await queue.GetQueueStatsAsync()).Dequeued); + await w.AbandonAsync(); + countdown.Signal(); - // TODO: We should verify that only one retry occurred. - await workItem.CompleteAsync(); + _logger.LogInformation("Finished Attempt {Attempt} to work on queue item, Metadata Attempts: {MetadataAttempts}", attempts, queueEntryMetadata.Attempts); + }); - if (_assertStats) - { - var stats = await queue.GetQueueStatsAsync(); - Assert.Equal(1, stats.Completed); - Assert.Equal(0, stats.Queued); - Assert.Equal(1, metricsCollector.GetSum("foundatio.simpleworkitem.completed")); - } - } - finally + await queue.EnqueueAsync(new SimpleWorkItem { - await CleanupQueueAsync(queue); - } - } + Data = "Hello" + }); - public virtual async Task CanDequeueEfficientlyAsync() - { - const int iterations = 100; - - var queue = GetQueue(runQueueMaintenance: false); - if (queue == null) - return; + await countdown.WaitAsync(waitTime); + Assert.Equal(0, countdown.CurrentCount); - try + if (_assertStats) { - await queue.DeleteQueueAsync(); - await AssertEmptyQueueAsync(queue); - await queue.EnqueueAsync(new SimpleWorkItem { Data = "Initialize queue to create more accurate metrics" }); - Assert.NotNull(await queue.DequeueAsync(TimeSpan.FromSeconds(1))); - - using var metricsCollector = new DiagnosticsMetricsCollector(FoundatioDiagnostics.Meter.Name, _logger); - - using var metrics = new InMemoryMetricsClient(new InMemoryMetricsClientOptions()); - queue.AttachBehavior(new MetricsQueueBehavior(metrics, reportCountsInterval: TimeSpan.FromMilliseconds(100), loggerFactory: Log)); - - _ = Task.Run(async () => - { - _logger.LogTrace("Starting enqueue loop"); - for (int index = 0; index < iterations; index++) - { - await SystemClock.SleepAsync(RandomData.GetInt(10, 30)); - await queue.EnqueueAsync(new SimpleWorkItem { Data = "Hello" }); - } - _logger.LogTrace("Finished enqueuing"); - }); + var stats = await queue.GetQueueStatsAsync(); + Assert.Equal(retryCount + 1, attempts); + Assert.Equal(0, stats.Completed); + Assert.Equal(0, stats.Queued); + Assert.Equal(0, stats.Errors); + Assert.Equal(retryCount + 1, stats.Dequeued); + Assert.Equal(retryCount + 1, stats.Abandoned); - _logger.LogTrace("Starting dequeue loop"); - for (int index = 0; index < iterations; index++) - { - var item = await queue.DequeueAsync(TimeSpan.FromSeconds(3)); - Assert.NotNull(item); - await item.CompleteAsync(); - } - _logger.LogTrace("Finished dequeuing"); + metricsCollector.RecordObservableInstruments(); + Assert.Equal(retryCount + 1, metricsCollector.GetSum("foundatio.simpleworkitem.dequeued")); + Assert.Equal(0, metricsCollector.GetSum("foundatio.simpleworkitem.completed")); + Assert.Equal(retryCount + 1, metricsCollector.GetSum("foundatio.simpleworkitem.abandoned")); - await metrics.FlushAsync(); - var timing = await metrics.GetTimerStatsAsync("foundatio.simpleworkitem.queuetime"); - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("AverageDuration: {AverageDuration}", timing.AverageDuration); - Assert.InRange(timing.AverageDuration, 0, 75); - Assert.InRange(metricsCollector.GetAvg("foundatio.simpleworkitem.queuetime"), 0, 75); - } - finally - { - await CleanupQueueAsync(queue); + Assert.Equal(0, metricsCollector.GetSum("foundatio.simpleworkitem.count")); } } - - public virtual async Task CanResumeDequeueEfficientlyAsync() + finally { - const int iterations = 10; - - var queue = GetQueue(runQueueMaintenance: false); - if (queue == null) - return; - - try - { - await queue.DeleteQueueAsync(); - await AssertEmptyQueueAsync(queue); - - using var metrics = new InMemoryMetricsClient(new InMemoryMetricsClientOptions()); - - for (int index = 0; index < iterations; index++) - await queue.EnqueueAsync(new SimpleWorkItem { Data = "Hello" }); - - using var secondQueue = GetQueue(runQueueMaintenance: false); - secondQueue.AttachBehavior(new MetricsQueueBehavior(metrics, reportCountsInterval: TimeSpan.FromMilliseconds(100), loggerFactory: Log)); + await CleanupQueueAsync(queue); + } + } - _logger.LogTrace("Starting dequeue loop"); - for (int index = 0; index < iterations; index++) - { - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("[{Index}] Calling Dequeue", index); - var item = await secondQueue.DequeueAsync(TimeSpan.FromSeconds(3)); - Assert.NotNull(item); - await item.CompleteAsync(); - } + /// + /// When a cancelled token is passed into Dequeue, it will only try to dequeue one time and then exit. + /// + /// + public virtual async Task CanDequeueWithCancelledTokenAsync() + { + var queue = GetQueue(); + if (queue == null) + return; - await metrics.FlushAsync(); // This won't flush metrics queue behaviors - var timing = await metrics.GetTimerStatsAsync("simpleworkitem.queuetime"); - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("TotalDuration: {TotalDuration} AverageDuration: {AverageDuration}", timing.TotalDuration, timing.AverageDuration); - Assert.InRange(timing.AverageDuration, 0, 75); - } - finally - { - await CleanupQueueAsync(queue); - } - } + using var metricsCollector = new DiagnosticsMetricsCollector(FoundatioDiagnostics.Meter.Name, _logger); - public virtual async Task CanQueueAndDequeueMultipleWorkItemsAsync() + try { - var queue = GetQueue(); - if (queue == null) - return; + await queue.DeleteQueueAsync(); + await AssertEmptyQueueAsync(queue); - try + await queue.EnqueueAsync(new SimpleWorkItem + { + Data = "Hello" + }); + if (_assertStats) { - using var metricsCollector = new DiagnosticsMetricsCollector(FoundatioDiagnostics.Meter.Name, _logger); + Assert.Equal(1, (await queue.GetQueueStatsAsync()).Enqueued); + Assert.Equal(1, metricsCollector.GetSum("foundatio.simpleworkitem.enqueued")); + } - await queue.DeleteQueueAsync(); - await AssertEmptyQueueAsync(queue); + var workItem = await queue.DequeueAsync(new CancellationToken(true)); + Assert.NotNull(workItem); + Assert.Equal("Hello", workItem.Value.Data); + Assert.Equal(1, (await queue.GetQueueStatsAsync()).Dequeued); - const int workItemCount = 25; - for (int i = 0; i < workItemCount; i++) - { - await queue.EnqueueAsync(new SimpleWorkItem - { - Data = "Hello" - }); - } - metricsCollector.RecordObservableInstruments(); - Assert.Equal(workItemCount, metricsCollector.GetMax("foundatio.simpleworkitem.count")); - Assert.Equal(workItemCount, (await queue.GetQueueStatsAsync()).Queued); + // TODO: We should verify that only one retry occurred. + await workItem.CompleteAsync(); - var sw = Stopwatch.StartNew(); - for (int i = 0; i < workItemCount; i++) - { - var workItem = await queue.DequeueAsync(TimeSpan.FromSeconds(5)); - Assert.NotNull(workItem); - Assert.Equal("Hello", workItem.Value.Data); - await workItem.CompleteAsync(); - } - sw.Stop(); - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Time {Elapsed:g}", sw.Elapsed); - Assert.InRange(sw.Elapsed.TotalSeconds, 0, 5); - - if (_assertStats) - { - var stats = await queue.GetQueueStatsAsync(); - Assert.Equal(workItemCount, stats.Dequeued); - Assert.Equal(workItemCount, stats.Completed); - Assert.Equal(0, stats.Queued); - } - } - finally + if (_assertStats) { - await CleanupQueueAsync(queue); + var stats = await queue.GetQueueStatsAsync(); + Assert.Equal(1, stats.Completed); + Assert.Equal(0, stats.Queued); + Assert.Equal(1, metricsCollector.GetSum("foundatio.simpleworkitem.completed")); } } - - public virtual async Task WillNotWaitForItemAsync() + finally { - var queue = GetQueue(); - if (queue == null) - return; - - try - { - await queue.DeleteQueueAsync(); - await AssertEmptyQueueAsync(queue); - - var sw = Stopwatch.StartNew(); - var workItem = await queue.DequeueAsync(TimeSpan.Zero); - sw.Stop(); - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Time {Elapsed:g}", sw.Elapsed); - Assert.Null(workItem); - Assert.InRange(sw.Elapsed.TotalMilliseconds, 0, 100); - } - finally - { - await CleanupQueueAsync(queue); - } + await CleanupQueueAsync(queue); } + } - public virtual async Task WillWaitForItemAsync() - { - Log.MinimumLevel = LogLevel.Trace; - var queue = GetQueue(); - if (queue == null) - return; + public virtual async Task CanDequeueEfficientlyAsync() + { + const int iterations = 100; - try - { - await queue.DeleteQueueAsync(); - await AssertEmptyQueueAsync(queue); + var queue = GetQueue(runQueueMaintenance: false); + if (queue == null) + return; - var sw = Stopwatch.StartNew(); - var workItem = await queue.DequeueAsync(TimeSpan.FromMilliseconds(100)); - sw.Stop(); - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Time {Elapsed:g}", sw.Elapsed); - Assert.Null(workItem); - Assert.InRange(sw.Elapsed, TimeSpan.FromMilliseconds(50), TimeSpan.FromMilliseconds(5000)); + try + { + await queue.DeleteQueueAsync(); + await AssertEmptyQueueAsync(queue); + await queue.EnqueueAsync(new SimpleWorkItem { Data = "Initialize queue to create more accurate metrics" }); + Assert.NotNull(await queue.DequeueAsync(TimeSpan.FromSeconds(1))); - _ = Task.Run(async () => - { - await SystemClock.SleepAsync(500); - await queue.EnqueueAsync(new SimpleWorkItem - { - Data = "Hello" - }); - }); + using var metricsCollector = new DiagnosticsMetricsCollector(FoundatioDiagnostics.Meter.Name, _logger); - sw.Restart(); - workItem = await queue.DequeueAsync(TimeSpan.FromSeconds(10)); - sw.Stop(); - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Time {Elapsed:g}", sw.Elapsed); - Assert.True(sw.Elapsed > TimeSpan.FromMilliseconds(400)); - Assert.NotNull(workItem); - await workItem.CompleteAsync(); + using var metrics = new InMemoryMetricsClient(new InMemoryMetricsClientOptions()); + queue.AttachBehavior(new MetricsQueueBehavior(metrics, reportCountsInterval: TimeSpan.FromMilliseconds(100), loggerFactory: Log)); - } - finally + _ = Task.Run(async () => + { + _logger.LogTrace("Starting enqueue loop"); + for (int index = 0; index < iterations; index++) + { + await SystemClock.SleepAsync(RandomData.GetInt(10, 30)); + await queue.EnqueueAsync(new SimpleWorkItem { Data = "Hello" }); + } + _logger.LogTrace("Finished enqueuing"); + }); + + _logger.LogTrace("Starting dequeue loop"); + for (int index = 0; index < iterations; index++) { - await CleanupQueueAsync(queue); + var item = await queue.DequeueAsync(TimeSpan.FromSeconds(3)); + Assert.NotNull(item); + await item.CompleteAsync(); } + _logger.LogTrace("Finished dequeuing"); + + await metrics.FlushAsync(); + var timing = await metrics.GetTimerStatsAsync("foundatio.simpleworkitem.queuetime"); + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("AverageDuration: {AverageDuration}", timing.AverageDuration); + Assert.InRange(timing.AverageDuration, 0, 75); + Assert.InRange(metricsCollector.GetAvg("foundatio.simpleworkitem.queuetime"), 0, 75); + } + finally + { + await CleanupQueueAsync(queue); } + } + + public virtual async Task CanResumeDequeueEfficientlyAsync() + { + const int iterations = 10; - public virtual async Task DequeueWaitWillGetSignaledAsync() + var queue = GetQueue(runQueueMaintenance: false); + if (queue == null) + return; + + try { - var queue = GetQueue(); - if (queue == null) - return; + await queue.DeleteQueueAsync(); + await AssertEmptyQueueAsync(queue); - try - { - await queue.DeleteQueueAsync(); - await AssertEmptyQueueAsync(queue); + using var metrics = new InMemoryMetricsClient(new InMemoryMetricsClientOptions()); - _ = Task.Run(async () => - { - await SystemClock.SleepAsync(250); - await queue.EnqueueAsync(new SimpleWorkItem - { - Data = "Hello" - }); - }); + for (int index = 0; index < iterations; index++) + await queue.EnqueueAsync(new SimpleWorkItem { Data = "Hello" }); - var sw = Stopwatch.StartNew(); - var workItem = await queue.DequeueAsync(TimeSpan.FromSeconds(2)); - sw.Stop(); - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Time {Elapsed:g}", sw.Elapsed); - Assert.NotNull(workItem); - Assert.InRange(sw.Elapsed.TotalSeconds, 0, 2); - } - finally + using var secondQueue = GetQueue(runQueueMaintenance: false); + secondQueue.AttachBehavior(new MetricsQueueBehavior(metrics, reportCountsInterval: TimeSpan.FromMilliseconds(100), loggerFactory: Log)); + + _logger.LogTrace("Starting dequeue loop"); + for (int index = 0; index < iterations; index++) { - await CleanupQueueAsync(queue); + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("[{Index}] Calling Dequeue", index); + var item = await secondQueue.DequeueAsync(TimeSpan.FromSeconds(3)); + Assert.NotNull(item); + await item.CompleteAsync(); } - } - public virtual async Task CanUseQueueWorkerAsync() + await metrics.FlushAsync(); // This won't flush metrics queue behaviors + var timing = await metrics.GetTimerStatsAsync("simpleworkitem.queuetime"); + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("TotalDuration: {TotalDuration} AverageDuration: {AverageDuration}", timing.TotalDuration, timing.AverageDuration); + Assert.InRange(timing.AverageDuration, 0, 75); + } + finally { - var queue = GetQueue(); - if (queue == null) - return; + await CleanupQueueAsync(queue); + } + } - try - { - await queue.DeleteQueueAsync(); - await AssertEmptyQueueAsync(queue); + public virtual async Task CanQueueAndDequeueMultipleWorkItemsAsync() + { + var queue = GetQueue(); + if (queue == null) + return; - var resetEvent = new AsyncManualResetEvent(false); - await queue.StartWorkingAsync(async w => - { - Assert.Equal("Hello", w.Value.Data); - await w.CompleteAsync(); - resetEvent.Set(); - }); + try + { + using var metricsCollector = new DiagnosticsMetricsCollector(FoundatioDiagnostics.Meter.Name, _logger); + await queue.DeleteQueueAsync(); + await AssertEmptyQueueAsync(queue); + + const int workItemCount = 25; + for (int i = 0; i < workItemCount; i++) + { await queue.EnqueueAsync(new SimpleWorkItem { Data = "Hello" }); + } + metricsCollector.RecordObservableInstruments(); + Assert.Equal(workItemCount, metricsCollector.GetMax("foundatio.simpleworkitem.count")); + Assert.Equal(workItemCount, (await queue.GetQueueStatsAsync()).Queued); - await resetEvent.WaitAsync(); - if (_assertStats) - { - var stats = await queue.GetQueueStatsAsync(); - Assert.Equal(1, stats.Completed); - Assert.Equal(0, stats.Queued); - Assert.Equal(0, stats.Errors); - } + var sw = Stopwatch.StartNew(); + for (int i = 0; i < workItemCount; i++) + { + var workItem = await queue.DequeueAsync(TimeSpan.FromSeconds(5)); + Assert.NotNull(workItem); + Assert.Equal("Hello", workItem.Value.Data); + await workItem.CompleteAsync(); } - finally + sw.Stop(); + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Time {Elapsed:g}", sw.Elapsed); + Assert.InRange(sw.Elapsed.TotalSeconds, 0, 5); + + if (_assertStats) { - await CleanupQueueAsync(queue); + var stats = await queue.GetQueueStatsAsync(); + Assert.Equal(workItemCount, stats.Dequeued); + Assert.Equal(workItemCount, stats.Completed); + Assert.Equal(0, stats.Queued); } } + finally + { + await CleanupQueueAsync(queue); + } + } + + public virtual async Task WillNotWaitForItemAsync() + { + var queue = GetQueue(); + if (queue == null) + return; - public virtual async Task CanHandleErrorInWorkerAsync() + try { - var queue = GetQueue(retries: 0); - if (queue == null) - return; + await queue.DeleteQueueAsync(); + await AssertEmptyQueueAsync(queue); + + var sw = Stopwatch.StartNew(); + var workItem = await queue.DequeueAsync(TimeSpan.Zero); + sw.Stop(); + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Time {Elapsed:g}", sw.Elapsed); + Assert.Null(workItem); + Assert.InRange(sw.Elapsed.TotalMilliseconds, 0, 100); + } + finally + { + await CleanupQueueAsync(queue); + } + } - try - { - await queue.DeleteQueueAsync(); - await AssertEmptyQueueAsync(queue); + public virtual async Task WillWaitForItemAsync() + { + Log.MinimumLevel = LogLevel.Trace; + var queue = GetQueue(); + if (queue == null) + return; + + try + { + await queue.DeleteQueueAsync(); + await AssertEmptyQueueAsync(queue); - using var metrics = new InMemoryMetricsClient(new InMemoryMetricsClientOptions { Buffered = false, LoggerFactory = Log }); + var sw = Stopwatch.StartNew(); + var workItem = await queue.DequeueAsync(TimeSpan.FromMilliseconds(100)); + sw.Stop(); + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Time {Elapsed:g}", sw.Elapsed); + Assert.Null(workItem); + Assert.InRange(sw.Elapsed, TimeSpan.FromMilliseconds(50), TimeSpan.FromMilliseconds(5000)); - queue.AttachBehavior(new MetricsQueueBehavior(metrics, reportCountsInterval: TimeSpan.FromMilliseconds(100), loggerFactory: Log)); - await queue.StartWorkingAsync(w => + _ = Task.Run(async () => + { + await SystemClock.SleepAsync(500); + await queue.EnqueueAsync(new SimpleWorkItem { - _logger.LogDebug("WorkAction"); - Assert.Equal("Hello", w.Value.Data); - throw new Exception(); + Data = "Hello" }); + }); - var resetEvent = new AsyncManualResetEvent(false); - using (queue.Abandoned.AddSyncHandler((o, args) => resetEvent.Set())) - { - await queue.EnqueueAsync(new SimpleWorkItem { Data = "Hello" }); - await resetEvent.WaitAsync(TimeSpan.FromSeconds(200)); + sw.Restart(); + workItem = await queue.DequeueAsync(TimeSpan.FromSeconds(10)); + sw.Stop(); + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Time {Elapsed:g}", sw.Elapsed); + Assert.True(sw.Elapsed > TimeSpan.FromMilliseconds(400)); + Assert.NotNull(workItem); + await workItem.CompleteAsync(); - await SystemClock.SleepAsync(100); // give time for the stats to reflect the changes. - - if (_assertStats) - { - var stats = await queue.GetQueueStatsAsync(); - if (_logger.IsEnabled(LogLevel.Information)) - _logger.LogInformation("Completed: {Completed} Errors: {Errors} Deadletter: {Deadletter} Working: {Working} ", stats.Completed, stats.Errors, stats.Deadletter, stats.Working); - Assert.Equal(0, stats.Completed); - Assert.Equal(1, stats.Errors); - Assert.Equal(1, stats.Deadletter); - } - } - } - finally - { - await CleanupQueueAsync(queue); - } } + finally + { + await CleanupQueueAsync(queue); + } + } + + public virtual async Task DequeueWaitWillGetSignaledAsync() + { + var queue = GetQueue(); + if (queue == null) + return; - public virtual async Task WorkItemsWillTimeoutAsync() + try { - Log.MinimumLevel = LogLevel.Trace; - Log.SetLogLevel("Foundatio.Queues.RedisQueue", LogLevel.Trace); - var queue = GetQueue(retryDelay: TimeSpan.Zero, workItemTimeout: TimeSpan.FromMilliseconds(50)); - if (queue == null) - return; + await queue.DeleteQueueAsync(); + await AssertEmptyQueueAsync(queue); - try + _ = Task.Run(async () => { - await queue.DeleteQueueAsync(); - await AssertEmptyQueueAsync(queue); - + await SystemClock.SleepAsync(250); await queue.EnqueueAsync(new SimpleWorkItem { Data = "Hello" }); - var workItem = await queue.DequeueAsync(TimeSpan.Zero); - Assert.NotNull(workItem); - Assert.Equal("Hello", workItem.Value.Data); + }); + + var sw = Stopwatch.StartNew(); + var workItem = await queue.DequeueAsync(TimeSpan.FromSeconds(2)); + sw.Stop(); + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Time {Elapsed:g}", sw.Elapsed); + Assert.NotNull(workItem); + Assert.InRange(sw.Elapsed.TotalSeconds, 0, 2); + } + finally + { + await CleanupQueueAsync(queue); + } + } - var sw = Stopwatch.StartNew(); - if (_assertStats) - { - // wait for the entry to be auto abandoned - do - { - var stats = await queue.GetQueueStatsAsync(); - if (stats.Abandoned > 0) - break; - await Task.Delay(250); - } while (sw.Elapsed < TimeSpan.FromSeconds(10)); - } + public virtual async Task CanUseQueueWorkerAsync() + { + var queue = GetQueue(); + if (queue == null) + return; - // should throw because the item has already been auto abandoned - if (_assertStats) - await Assert.ThrowsAnyAsync(async () => await workItem.CompleteAsync().AnyContext()); + try + { + await queue.DeleteQueueAsync(); + await AssertEmptyQueueAsync(queue); - sw = Stopwatch.StartNew(); - workItem = await queue.DequeueAsync(TimeSpan.FromSeconds(5)); - sw.Stop(); - _logger.LogTrace("Time {Elapsed:g}", sw.Elapsed); - Assert.NotNull(workItem); - await workItem.CompleteAsync(); - if (_assertStats) - Assert.Equal(0, (await queue.GetQueueStatsAsync()).Queued); - } - finally + var resetEvent = new AsyncManualResetEvent(false); + await queue.StartWorkingAsync(async w => + { + Assert.Equal("Hello", w.Value.Data); + await w.CompleteAsync(); + resetEvent.Set(); + }); + + await queue.EnqueueAsync(new SimpleWorkItem + { + Data = "Hello" + }); + + await resetEvent.WaitAsync(); + if (_assertStats) { - await CleanupQueueAsync(queue); + var stats = await queue.GetQueueStatsAsync(); + Assert.Equal(1, stats.Completed); + Assert.Equal(0, stats.Queued); + Assert.Equal(0, stats.Errors); } } - - public virtual async Task WorkItemsWillGetMovedToDeadletterAsync() + finally { - var queue = GetQueue(retryDelay: TimeSpan.Zero); - if (queue == null) - return; + await CleanupQueueAsync(queue); + } + } - try - { - await queue.DeleteQueueAsync(); - await AssertEmptyQueueAsync(queue); + public virtual async Task CanHandleErrorInWorkerAsync() + { + var queue = GetQueue(retries: 0); + if (queue == null) + return; - await queue.EnqueueAsync(new SimpleWorkItem - { - Data = "Hello" - }); - var workItem = await queue.DequeueAsync(TimeSpan.Zero); - Assert.Equal("Hello", workItem.Value.Data); - Assert.Equal(1, (await queue.GetQueueStatsAsync()).Dequeued); + try + { + await queue.DeleteQueueAsync(); + await AssertEmptyQueueAsync(queue); - await workItem.AbandonAsync(); - Assert.Equal(1, (await queue.GetQueueStatsAsync()).Abandoned); + using var metrics = new InMemoryMetricsClient(new InMemoryMetricsClientOptions { Buffered = false, LoggerFactory = Log }); - // work item should be retried 1 time. - workItem = await queue.DequeueAsync(TimeSpan.FromSeconds(5)); - Assert.NotNull(workItem); - Assert.Equal("Hello", workItem.Value.Data); - Assert.Equal(2, (await queue.GetQueueStatsAsync()).Dequeued); + queue.AttachBehavior(new MetricsQueueBehavior(metrics, reportCountsInterval: TimeSpan.FromMilliseconds(100), loggerFactory: Log)); + await queue.StartWorkingAsync(w => + { + _logger.LogDebug("WorkAction"); + Assert.Equal("Hello", w.Value.Data); + throw new Exception(); + }); + + var resetEvent = new AsyncManualResetEvent(false); + using (queue.Abandoned.AddSyncHandler((o, args) => resetEvent.Set())) + { + await queue.EnqueueAsync(new SimpleWorkItem { Data = "Hello" }); + await resetEvent.WaitAsync(TimeSpan.FromSeconds(200)); - await workItem.AbandonAsync(); + await SystemClock.SleepAsync(100); // give time for the stats to reflect the changes. if (_assertStats) { - // work item should be moved to deadletter _queue after retries. var stats = await queue.GetQueueStatsAsync(); + if (_logger.IsEnabled(LogLevel.Information)) + _logger.LogInformation("Completed: {Completed} Errors: {Errors} Deadletter: {Deadletter} Working: {Working} ", stats.Completed, stats.Errors, stats.Deadletter, stats.Working); + Assert.Equal(0, stats.Completed); + Assert.Equal(1, stats.Errors); Assert.Equal(1, stats.Deadletter); - Assert.Equal(2, stats.Abandoned); } } - finally + } + finally + { + await CleanupQueueAsync(queue); + } + } + + public virtual async Task WorkItemsWillTimeoutAsync() + { + Log.MinimumLevel = LogLevel.Trace; + Log.SetLogLevel("Foundatio.Queues.RedisQueue", LogLevel.Trace); + var queue = GetQueue(retryDelay: TimeSpan.Zero, workItemTimeout: TimeSpan.FromMilliseconds(50)); + if (queue == null) + return; + + try + { + await queue.DeleteQueueAsync(); + await AssertEmptyQueueAsync(queue); + + await queue.EnqueueAsync(new SimpleWorkItem + { + Data = "Hello" + }); + var workItem = await queue.DequeueAsync(TimeSpan.Zero); + Assert.NotNull(workItem); + Assert.Equal("Hello", workItem.Value.Data); + + var sw = Stopwatch.StartNew(); + if (_assertStats) { - await CleanupQueueAsync(queue); + // wait for the entry to be auto abandoned + do + { + var stats = await queue.GetQueueStatsAsync(); + if (stats.Abandoned > 0) + break; + await Task.Delay(250); + } while (sw.Elapsed < TimeSpan.FromSeconds(10)); } + + // should throw because the item has already been auto abandoned + if (_assertStats) + await Assert.ThrowsAnyAsync(async () => await workItem.CompleteAsync().AnyContext()); + + sw = Stopwatch.StartNew(); + workItem = await queue.DequeueAsync(TimeSpan.FromSeconds(5)); + sw.Stop(); + _logger.LogTrace("Time {Elapsed:g}", sw.Elapsed); + Assert.NotNull(workItem); + await workItem.CompleteAsync(); + if (_assertStats) + Assert.Equal(0, (await queue.GetQueueStatsAsync()).Queued); + } + finally + { + await CleanupQueueAsync(queue); } + } - public virtual async Task CanAutoCompleteWorkerAsync() + public virtual async Task WorkItemsWillGetMovedToDeadletterAsync() + { + var queue = GetQueue(retryDelay: TimeSpan.Zero); + if (queue == null) + return; + + try { - var queue = GetQueue(); - if (queue == null) - return; + await queue.DeleteQueueAsync(); + await AssertEmptyQueueAsync(queue); - try + await queue.EnqueueAsync(new SimpleWorkItem { - await queue.DeleteQueueAsync(); - await AssertEmptyQueueAsync(queue); + Data = "Hello" + }); + var workItem = await queue.DequeueAsync(TimeSpan.Zero); + Assert.Equal("Hello", workItem.Value.Data); + Assert.Equal(1, (await queue.GetQueueStatsAsync()).Dequeued); - var resetEvent = new AsyncManualResetEvent(false); - await queue.StartWorkingAsync(w => - { - Assert.Equal("Hello", w.Value.Data); - return Task.CompletedTask; - }, true); + await workItem.AbandonAsync(); + Assert.Equal(1, (await queue.GetQueueStatsAsync()).Abandoned); - using (queue.Completed.AddSyncHandler((s, e) => { resetEvent.Set(); })) - { - await queue.EnqueueAsync(new SimpleWorkItem { Data = "Hello" }); + // work item should be retried 1 time. + workItem = await queue.DequeueAsync(TimeSpan.FromSeconds(5)); + Assert.NotNull(workItem); + Assert.Equal("Hello", workItem.Value.Data); + Assert.Equal(2, (await queue.GetQueueStatsAsync()).Dequeued); - Assert.Equal(1, (await queue.GetQueueStatsAsync()).Enqueued); - await resetEvent.WaitAsync(TimeSpan.FromSeconds(2)); + await workItem.AbandonAsync(); - if (_assertStats) - { - var stats = await queue.GetQueueStatsAsync(); - Assert.Equal(0, stats.Queued); - Assert.Equal(0, stats.Errors); - Assert.Equal(1, stats.Completed); - } - } - } - finally + if (_assertStats) { - await CleanupQueueAsync(queue); + // work item should be moved to deadletter _queue after retries. + var stats = await queue.GetQueueStatsAsync(); + Assert.Equal(1, stats.Deadletter); + Assert.Equal(2, stats.Abandoned); } } - - public virtual async Task CanHaveMultipleQueueInstancesAsync() + finally { - var queue = GetQueue(retries: 0, retryDelay: TimeSpan.Zero); - if (queue == null) - return; - - try - { - await queue.DeleteQueueAsync(); - await AssertEmptyQueueAsync(queue); + await CleanupQueueAsync(queue); + } + } - const int workItemCount = 500; - const int workerCount = 3; - var countdown = new AsyncCountdownEvent(workItemCount); - var info = new WorkInfo(); - var workers = new List> { queue }; + public virtual async Task CanAutoCompleteWorkerAsync() + { + var queue = GetQueue(); + if (queue == null) + return; - try - { - for (int i = 0; i < workerCount; i++) - { - var q = GetQueue(retries: 0, retryDelay: TimeSpan.Zero); - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Queue Id: {Id}, I: {Instance}", q.QueueId, i); - await q.StartWorkingAsync(w => DoWorkAsync(w, countdown, info)); - workers.Add(q); - } + try + { + await queue.DeleteQueueAsync(); + await AssertEmptyQueueAsync(queue); - await Run.InParallelAsync(workItemCount, async i => - { - string id = await queue.EnqueueAsync(new SimpleWorkItem - { - Data = "Hello", - Id = i - }); - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Enqueued Index: {Instance} Id: {Id}", i, id); - }); + var resetEvent = new AsyncManualResetEvent(false); + await queue.StartWorkingAsync(w => + { + Assert.Equal("Hello", w.Value.Data); + return Task.CompletedTask; + }, true); - await countdown.WaitAsync(); - await SystemClock.SleepAsync(50); + using (queue.Completed.AddSyncHandler((s, e) => { resetEvent.Set(); })) + { + await queue.EnqueueAsync(new SimpleWorkItem { Data = "Hello" }); - if (_logger.IsEnabled(LogLevel.Information)) - _logger.LogInformation("Work Info Stats: Completed: {Completed} Abandoned: {Abandoned} Error: {Errors}", info.CompletedCount, info.AbandonCount, info.ErrorCount); - Assert.Equal(workItemCount, info.CompletedCount + info.AbandonCount + info.ErrorCount); + Assert.Equal(1, (await queue.GetQueueStatsAsync()).Enqueued); + await resetEvent.WaitAsync(TimeSpan.FromSeconds(2)); - // In memory queue doesn't share state. - if (queue.GetType() == typeof(InMemoryQueue)) - { - var stats = await queue.GetQueueStatsAsync(); - Assert.Equal(0, stats.Working); - Assert.Equal(0, stats.Timeouts); - Assert.Equal(workItemCount, stats.Enqueued); - Assert.Equal(workItemCount, stats.Dequeued); - Assert.Equal(info.CompletedCount, stats.Completed); - Assert.Equal(info.ErrorCount, stats.Errors); - Assert.Equal(info.AbandonCount, stats.Abandoned - info.ErrorCount); - Assert.Equal(info.AbandonCount + stats.Errors, stats.Deadletter); - } - else if (_assertStats) - { - var workerStats = new List(); - for (int i = 0; i < workers.Count; i++) - { - var stats = await workers[i].GetQueueStatsAsync(); - if (_logger.IsEnabled(LogLevel.Information)) - _logger.LogInformation("Worker#{Id} Working: {Working} Completed: {Completed} Abandoned: {Abandoned} Error: {Errors} Deadletter: {Deadletter}", i, stats.Working, stats.Completed, stats.Abandoned, stats.Errors, stats.Deadletter); - workerStats.Add(stats); - } - - Assert.Equal(info.CompletedCount, workerStats.Sum(s => s.Completed)); - Assert.Equal(info.ErrorCount, workerStats.Sum(s => s.Errors)); - Assert.Equal(info.AbandonCount, workerStats.Sum(s => s.Abandoned) - info.ErrorCount); - Assert.Equal(info.AbandonCount + workerStats.Sum(s => s.Errors), (workerStats.LastOrDefault()?.Deadletter ?? 0)); - //Expected: 260 - //Actual: 125 - } - } - finally + if (_assertStats) { - foreach (var q in workers) - await CleanupQueueAsync(q); + var stats = await queue.GetQueueStatsAsync(); + Assert.Equal(0, stats.Queued); + Assert.Equal(0, stats.Errors); + Assert.Equal(1, stats.Completed); } } - finally - { - await CleanupQueueAsync(queue); - } } + finally + { + await CleanupQueueAsync(queue); + } + } + + public virtual async Task CanHaveMultipleQueueInstancesAsync() + { + var queue = GetQueue(retries: 0, retryDelay: TimeSpan.Zero); + if (queue == null) + return; - public virtual async Task CanDelayRetryAsync() + try { - var queue = GetQueue(workItemTimeout: TimeSpan.FromMilliseconds(500), retryDelay: TimeSpan.FromSeconds(1)); - if (queue == null) - return; + await queue.DeleteQueueAsync(); + await AssertEmptyQueueAsync(queue); + + const int workItemCount = 500; + const int workerCount = 3; + var countdown = new AsyncCountdownEvent(workItemCount); + var info = new WorkInfo(); + var workers = new List> { queue }; try { - await queue.DeleteQueueAsync(); - await AssertEmptyQueueAsync(queue); + for (int i = 0; i < workerCount; i++) + { + var q = GetQueue(retries: 0, retryDelay: TimeSpan.Zero); + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Queue Id: {Id}, I: {Instance}", q.QueueId, i); + await q.StartWorkingAsync(w => DoWorkAsync(w, countdown, info)); + workers.Add(q); + } - await queue.EnqueueAsync(new SimpleWorkItem + await Run.InParallelAsync(workItemCount, async i => { - Data = "Hello" + string id = await queue.EnqueueAsync(new SimpleWorkItem + { + Data = "Hello", + Id = i + }); + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Enqueued Index: {Instance} Id: {Id}", i, id); }); - var workItem = await queue.DequeueAsync(TimeSpan.Zero); - Assert.NotNull(workItem); - Assert.Equal("Hello", workItem.Value.Data); + await countdown.WaitAsync(); + await SystemClock.SleepAsync(50); - var startTime = SystemClock.UtcNow; - await workItem.AbandonAsync(); - Assert.Equal(1, (await queue.GetQueueStatsAsync()).Abandoned); + if (_logger.IsEnabled(LogLevel.Information)) + _logger.LogInformation("Work Info Stats: Completed: {Completed} Abandoned: {Abandoned} Error: {Errors}", info.CompletedCount, info.AbandonCount, info.ErrorCount); + Assert.Equal(workItemCount, info.CompletedCount + info.AbandonCount + info.ErrorCount); - workItem = await queue.DequeueAsync(TimeSpan.FromSeconds(5)); - var elapsed = SystemClock.UtcNow.Subtract(startTime); - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Time {Elapsed}", elapsed); - Assert.NotNull(workItem); - Assert.True(elapsed > TimeSpan.FromSeconds(.95)); - await workItem.CompleteAsync(); + // In memory queue doesn't share state. + if (queue.GetType() == typeof(InMemoryQueue)) + { + var stats = await queue.GetQueueStatsAsync(); + Assert.Equal(0, stats.Working); + Assert.Equal(0, stats.Timeouts); + Assert.Equal(workItemCount, stats.Enqueued); + Assert.Equal(workItemCount, stats.Dequeued); + Assert.Equal(info.CompletedCount, stats.Completed); + Assert.Equal(info.ErrorCount, stats.Errors); + Assert.Equal(info.AbandonCount, stats.Abandoned - info.ErrorCount); + Assert.Equal(info.AbandonCount + stats.Errors, stats.Deadletter); + } + else if (_assertStats) + { + var workerStats = new List(); + for (int i = 0; i < workers.Count; i++) + { + var stats = await workers[i].GetQueueStatsAsync(); + if (_logger.IsEnabled(LogLevel.Information)) + _logger.LogInformation("Worker#{Id} Working: {Working} Completed: {Completed} Abandoned: {Abandoned} Error: {Errors} Deadletter: {Deadletter}", i, stats.Working, stats.Completed, stats.Abandoned, stats.Errors, stats.Deadletter); + workerStats.Add(stats); + } - if (_assertStats) - Assert.Equal(0, (await queue.GetQueueStatsAsync()).Queued); + Assert.Equal(info.CompletedCount, workerStats.Sum(s => s.Completed)); + Assert.Equal(info.ErrorCount, workerStats.Sum(s => s.Errors)); + Assert.Equal(info.AbandonCount, workerStats.Sum(s => s.Abandoned) - info.ErrorCount); + Assert.Equal(info.AbandonCount + workerStats.Sum(s => s.Errors), (workerStats.LastOrDefault()?.Deadletter ?? 0)); + //Expected: 260 + //Actual: 125 + } } finally { - await CleanupQueueAsync(queue); + foreach (var q in workers) + await CleanupQueueAsync(q); } } - - public virtual async Task CanRunWorkItemWithMetricsAsync() + finally { - int completedCount = 0; + await CleanupQueueAsync(queue); + } + } - using var metrics = new InMemoryMetricsClient(new InMemoryMetricsClientOptions { Buffered = false, LoggerFactory = Log }); + public virtual async Task CanDelayRetryAsync() + { + var queue = GetQueue(workItemTimeout: TimeSpan.FromMilliseconds(500), retryDelay: TimeSpan.FromSeconds(1)); + if (queue == null) + return; - var behavior = new MetricsQueueBehavior(metrics, "metric", TimeSpan.FromMilliseconds(100), loggerFactory: Log); - var options = new InMemoryQueueOptions { Behaviors = new[] { behavior }, LoggerFactory = Log }; - using var queue = new InMemoryQueue(options); + try + { + await queue.DeleteQueueAsync(); + await AssertEmptyQueueAsync(queue); - Task Handler(object sender, CompletedEventArgs e) + await queue.EnqueueAsync(new SimpleWorkItem { - completedCount++; - return Task.CompletedTask; - } + Data = "Hello" + }); - using (queue.Completed.AddHandler(Handler)) - { - _logger.LogTrace("Before enqueue"); - await queue.EnqueueAsync(new SimpleWorkItem { Id = 1, Data = "Testing" }); - await queue.EnqueueAsync(new SimpleWorkItem { Id = 2, Data = "Testing" }); - await queue.EnqueueAsync(new SimpleWorkItem { Id = 3, Data = "Testing" }); + var workItem = await queue.DequeueAsync(TimeSpan.Zero); + Assert.NotNull(workItem); + Assert.Equal("Hello", workItem.Value.Data); - await SystemClock.SleepAsync(100); + var startTime = SystemClock.UtcNow; + await workItem.AbandonAsync(); + Assert.Equal(1, (await queue.GetQueueStatsAsync()).Abandoned); - _logger.LogTrace("Before dequeue"); - var item = await queue.DequeueAsync(); - await item.CompleteAsync(); + workItem = await queue.DequeueAsync(TimeSpan.FromSeconds(5)); + var elapsed = SystemClock.UtcNow.Subtract(startTime); + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Time {Elapsed}", elapsed); + Assert.NotNull(workItem); + Assert.True(elapsed > TimeSpan.FromSeconds(.95)); + await workItem.CompleteAsync(); - item = await queue.DequeueAsync(); - await item.CompleteAsync(); + if (_assertStats) + Assert.Equal(0, (await queue.GetQueueStatsAsync()).Queued); + } + finally + { + await CleanupQueueAsync(queue); + } + } + + public virtual async Task CanRunWorkItemWithMetricsAsync() + { + int completedCount = 0; - item = await queue.DequeueAsync(); - await item.AbandonAsync(); + using var metrics = new InMemoryMetricsClient(new InMemoryMetricsClientOptions { Buffered = false, LoggerFactory = Log }); - _logger.LogTrace("Before asserts"); - Assert.Equal(2, completedCount); + var behavior = new MetricsQueueBehavior(metrics, "metric", TimeSpan.FromMilliseconds(100), loggerFactory: Log); + var options = new InMemoryQueueOptions { Behaviors = new[] { behavior }, LoggerFactory = Log }; + using var queue = new InMemoryQueue(options); - await SystemClock.SleepAsync(100); // flush metrics queue behaviors - await metrics.FlushAsync(); - Assert.InRange((await metrics.GetGaugeStatsAsync("metric.workitemdata.count")).Max, 1, 3); - Assert.InRange((await metrics.GetGaugeStatsAsync("metric.workitemdata.working")).Max, 0, 1); + Task Handler(object sender, CompletedEventArgs e) + { + completedCount++; + return Task.CompletedTask; + } + + using (queue.Completed.AddHandler(Handler)) + { + _logger.LogTrace("Before enqueue"); + await queue.EnqueueAsync(new SimpleWorkItem { Id = 1, Data = "Testing" }); + await queue.EnqueueAsync(new SimpleWorkItem { Id = 2, Data = "Testing" }); + await queue.EnqueueAsync(new SimpleWorkItem { Id = 3, Data = "Testing" }); - Assert.Equal(3, await metrics.GetCounterCountAsync("metric.workitemdata.simple.enqueued")); - Assert.Equal(3, await metrics.GetCounterCountAsync("metric.workitemdata.enqueued")); + await SystemClock.SleepAsync(100); - Assert.Equal(3, await metrics.GetCounterCountAsync("metric.workitemdata.simple.dequeued")); - Assert.Equal(3, await metrics.GetCounterCountAsync("metric.workitemdata.dequeued")); + _logger.LogTrace("Before dequeue"); + var item = await queue.DequeueAsync(); + await item.CompleteAsync(); - Assert.Equal(2, await metrics.GetCounterCountAsync("metric.workitemdata.simple.completed")); - Assert.Equal(2, await metrics.GetCounterCountAsync("metric.workitemdata.completed")); + item = await queue.DequeueAsync(); + await item.CompleteAsync(); - Assert.Equal(1, await metrics.GetCounterCountAsync("metric.workitemdata.simple.abandoned")); - Assert.Equal(1, await metrics.GetCounterCountAsync("metric.workitemdata.abandoned")); + item = await queue.DequeueAsync(); + await item.AbandonAsync(); - var queueTiming = await metrics.GetTimerStatsAsync("metric.workitemdata.simple.queuetime"); - Assert.Equal(3, queueTiming.Count); - queueTiming = await metrics.GetTimerStatsAsync("metric.workitemdata.queuetime"); - Assert.Equal(3, queueTiming.Count); + _logger.LogTrace("Before asserts"); + Assert.Equal(2, completedCount); - var processTiming = await metrics.GetTimerStatsAsync("metric.workitemdata.simple.processtime"); - Assert.Equal(3, processTiming.Count); - processTiming = await metrics.GetTimerStatsAsync("metric.workitemdata.processtime"); - Assert.Equal(3, processTiming.Count); + await SystemClock.SleepAsync(100); // flush metrics queue behaviors + await metrics.FlushAsync(); + Assert.InRange((await metrics.GetGaugeStatsAsync("metric.workitemdata.count")).Max, 1, 3); + Assert.InRange((await metrics.GetGaugeStatsAsync("metric.workitemdata.working")).Max, 0, 1); - if (_assertStats) - { - var queueStats = await metrics.GetQueueStatsAsync("metric.workitemdata"); - Assert.Equal(3, queueStats.Enqueued.Count); - Assert.Equal(3, queueStats.Dequeued.Count); - Assert.Equal(2, queueStats.Completed.Count); - Assert.Equal(1, queueStats.Abandoned.Count); - Assert.InRange(queueStats.Count.Max, 1, 3); - Assert.InRange(queueStats.Working.Max, 0, 1); - - var subQueueStats = await metrics.GetQueueStatsAsync("metric.workitemdata", "simple"); - Assert.Equal(3, subQueueStats.Enqueued.Count); - Assert.Equal(3, subQueueStats.Dequeued.Count); - Assert.Equal(2, subQueueStats.Completed.Count); - Assert.Equal(1, subQueueStats.Abandoned.Count); - } - } - } + Assert.Equal(3, await metrics.GetCounterCountAsync("metric.workitemdata.simple.enqueued")); + Assert.Equal(3, await metrics.GetCounterCountAsync("metric.workitemdata.enqueued")); - public virtual async Task CanRenewLockAsync() - { - Log.SetLogLevel>(LogLevel.Trace); + Assert.Equal(3, await metrics.GetCounterCountAsync("metric.workitemdata.simple.dequeued")); + Assert.Equal(3, await metrics.GetCounterCountAsync("metric.workitemdata.dequeued")); - // Need large value to reproduce this test - var workItemTimeout = TimeSpan.FromSeconds(1); - // Slightly shorter than the timeout to ensure we haven't lost the lock - var renewWait = TimeSpan.FromSeconds(workItemTimeout.TotalSeconds * .25d); + Assert.Equal(2, await metrics.GetCounterCountAsync("metric.workitemdata.simple.completed")); + Assert.Equal(2, await metrics.GetCounterCountAsync("metric.workitemdata.completed")); - var queue = GetQueue(retryDelay: TimeSpan.Zero, workItemTimeout: workItemTimeout); - if (queue == null) - return; + Assert.Equal(1, await metrics.GetCounterCountAsync("metric.workitemdata.simple.abandoned")); + Assert.Equal(1, await metrics.GetCounterCountAsync("metric.workitemdata.abandoned")); - try - { - await queue.DeleteQueueAsync(); - await AssertEmptyQueueAsync(queue); + var queueTiming = await metrics.GetTimerStatsAsync("metric.workitemdata.simple.queuetime"); + Assert.Equal(3, queueTiming.Count); + queueTiming = await metrics.GetTimerStatsAsync("metric.workitemdata.queuetime"); + Assert.Equal(3, queueTiming.Count); - await queue.EnqueueAsync(new SimpleWorkItem - { - Data = "Hello" - }); - var entry = await queue.DequeueAsync(TimeSpan.Zero); - Assert.NotNull(entry); - Assert.Equal("Hello", entry.Value.Data); - - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Waiting for {RenewWait:g} before renewing lock", renewWait); - await SystemClock.SleepAsync(renewWait); - _logger.LogTrace("Renewing lock"); - await entry.RenewLockAsync(); - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Waiting for {RenewWait:g} to see if lock was renewed", renewWait); - await SystemClock.SleepAsync(renewWait); - - // We shouldn't get another item here if RenewLock works. - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Attempting to dequeue item that shouldn't exist"); - var nullWorkItem = await queue.DequeueAsync(TimeSpan.Zero); - Assert.Null(nullWorkItem); - await entry.CompleteAsync(); + var processTiming = await metrics.GetTimerStatsAsync("metric.workitemdata.simple.processtime"); + Assert.Equal(3, processTiming.Count); + processTiming = await metrics.GetTimerStatsAsync("metric.workitemdata.processtime"); + Assert.Equal(3, processTiming.Count); - if (_assertStats) - Assert.Equal(0, (await queue.GetQueueStatsAsync()).Queued); - } - finally + if (_assertStats) { - await CleanupQueueAsync(queue); + var queueStats = await metrics.GetQueueStatsAsync("metric.workitemdata"); + Assert.Equal(3, queueStats.Enqueued.Count); + Assert.Equal(3, queueStats.Dequeued.Count); + Assert.Equal(2, queueStats.Completed.Count); + Assert.Equal(1, queueStats.Abandoned.Count); + Assert.InRange(queueStats.Count.Max, 1, 3); + Assert.InRange(queueStats.Working.Max, 0, 1); + + var subQueueStats = await metrics.GetQueueStatsAsync("metric.workitemdata", "simple"); + Assert.Equal(3, subQueueStats.Enqueued.Count); + Assert.Equal(3, subQueueStats.Dequeued.Count); + Assert.Equal(2, subQueueStats.Completed.Count); + Assert.Equal(1, subQueueStats.Abandoned.Count); } } + } + + public virtual async Task CanRenewLockAsync() + { + Log.SetLogLevel>(LogLevel.Trace); + + // Need large value to reproduce this test + var workItemTimeout = TimeSpan.FromSeconds(1); + // Slightly shorter than the timeout to ensure we haven't lost the lock + var renewWait = TimeSpan.FromSeconds(workItemTimeout.TotalSeconds * .25d); - public virtual async Task CanAbandonQueueEntryOnceAsync() + var queue = GetQueue(retryDelay: TimeSpan.Zero, workItemTimeout: workItemTimeout); + if (queue == null) + return; + + try { - var queue = GetQueue(); - if (queue == null) - return; + await queue.DeleteQueueAsync(); + await AssertEmptyQueueAsync(queue); - try + await queue.EnqueueAsync(new SimpleWorkItem { - await queue.DeleteQueueAsync(); - await AssertEmptyQueueAsync(queue); + Data = "Hello" + }); + var entry = await queue.DequeueAsync(TimeSpan.Zero); + Assert.NotNull(entry); + Assert.Equal("Hello", entry.Value.Data); + + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Waiting for {RenewWait:g} before renewing lock", renewWait); + await SystemClock.SleepAsync(renewWait); + _logger.LogTrace("Renewing lock"); + await entry.RenewLockAsync(); + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Waiting for {RenewWait:g} to see if lock was renewed", renewWait); + await SystemClock.SleepAsync(renewWait); + + // We shouldn't get another item here if RenewLock works. + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Attempting to dequeue item that shouldn't exist"); + var nullWorkItem = await queue.DequeueAsync(TimeSpan.Zero); + Assert.Null(nullWorkItem); + await entry.CompleteAsync(); - await queue.EnqueueAsync(new SimpleWorkItem { Data = "Hello" }); - Assert.Equal(1, (await queue.GetQueueStatsAsync()).Enqueued); + if (_assertStats) + Assert.Equal(0, (await queue.GetQueueStatsAsync()).Queued); + } + finally + { + await CleanupQueueAsync(queue); + } + } - var workItem = await queue.DequeueAsync(TimeSpan.Zero); - Assert.NotNull(workItem); - Assert.Equal("Hello", workItem.Value.Data); - Assert.Equal(1, (await queue.GetQueueStatsAsync()).Dequeued); + public virtual async Task CanAbandonQueueEntryOnceAsync() + { + var queue = GetQueue(); + if (queue == null) + return; - await workItem.AbandonAsync(); - Assert.True(workItem.IsAbandoned); - Assert.False(workItem.IsCompleted); - await Assert.ThrowsAnyAsync(() => workItem.AbandonAsync()); - await Assert.ThrowsAnyAsync(() => workItem.CompleteAsync()); - await Assert.ThrowsAnyAsync(() => workItem.CompleteAsync()); + try + { + await queue.DeleteQueueAsync(); + await AssertEmptyQueueAsync(queue); - if (_assertStats) - { - var stats = await queue.GetQueueStatsAsync(); - Assert.Equal(1, stats.Abandoned); - Assert.Equal(0, stats.Completed); - Assert.Equal(0, stats.Deadletter); - Assert.Equal(1, stats.Dequeued); - Assert.Equal(1, stats.Enqueued); - Assert.Equal(0, stats.Errors); - Assert.InRange(stats.Queued, 0, 1); - Assert.Equal(0, stats.Timeouts); - Assert.Equal(0, stats.Working); - } + await queue.EnqueueAsync(new SimpleWorkItem { Data = "Hello" }); + Assert.Equal(1, (await queue.GetQueueStatsAsync()).Enqueued); - if (workItem is QueueEntry queueEntry) - Assert.Equal(1, queueEntry.Attempts); + var workItem = await queue.DequeueAsync(TimeSpan.Zero); + Assert.NotNull(workItem); + Assert.Equal("Hello", workItem.Value.Data); + Assert.Equal(1, (await queue.GetQueueStatsAsync()).Dequeued); - await queue.EnqueueAsync(new SimpleWorkItem { Data = "Hello" }); - workItem = await queue.DequeueAsync(TimeSpan.Zero); - - await queue.AbandonAsync(workItem); - Assert.True(workItem.IsAbandoned); - Assert.False(workItem.IsCompleted); - await Assert.ThrowsAnyAsync(() => workItem.CompleteAsync()); - await Assert.ThrowsAnyAsync(() => workItem.AbandonAsync()); - await Assert.ThrowsAnyAsync(() => queue.AbandonAsync(workItem)); - await Assert.ThrowsAnyAsync(() => queue.CompleteAsync(workItem)); - } - finally + await workItem.AbandonAsync(); + Assert.True(workItem.IsAbandoned); + Assert.False(workItem.IsCompleted); + await Assert.ThrowsAnyAsync(() => workItem.AbandonAsync()); + await Assert.ThrowsAnyAsync(() => workItem.CompleteAsync()); + await Assert.ThrowsAnyAsync(() => workItem.CompleteAsync()); + + if (_assertStats) { - await CleanupQueueAsync(queue); + var stats = await queue.GetQueueStatsAsync(); + Assert.Equal(1, stats.Abandoned); + Assert.Equal(0, stats.Completed); + Assert.Equal(0, stats.Deadletter); + Assert.Equal(1, stats.Dequeued); + Assert.Equal(1, stats.Enqueued); + Assert.Equal(0, stats.Errors); + Assert.InRange(stats.Queued, 0, 1); + Assert.Equal(0, stats.Timeouts); + Assert.Equal(0, stats.Working); } - } - public virtual async Task CanCompleteQueueEntryOnceAsync() + if (workItem is QueueEntry queueEntry) + Assert.Equal(1, queueEntry.Attempts); + + await queue.EnqueueAsync(new SimpleWorkItem { Data = "Hello" }); + workItem = await queue.DequeueAsync(TimeSpan.Zero); + + await queue.AbandonAsync(workItem); + Assert.True(workItem.IsAbandoned); + Assert.False(workItem.IsCompleted); + await Assert.ThrowsAnyAsync(() => workItem.CompleteAsync()); + await Assert.ThrowsAnyAsync(() => workItem.AbandonAsync()); + await Assert.ThrowsAnyAsync(() => queue.AbandonAsync(workItem)); + await Assert.ThrowsAnyAsync(() => queue.CompleteAsync(workItem)); + } + finally { - var queue = GetQueue(); - if (queue == null) - return; + await CleanupQueueAsync(queue); + } + } - try - { - await queue.DeleteQueueAsync(); - await queue.EnqueueAsync(new SimpleWorkItem { Data = "Hello" }); + public virtual async Task CanCompleteQueueEntryOnceAsync() + { + var queue = GetQueue(); + if (queue == null) + return; - Assert.Equal(1, (await queue.GetQueueStatsAsync()).Enqueued); + try + { + await queue.DeleteQueueAsync(); + await queue.EnqueueAsync(new SimpleWorkItem { Data = "Hello" }); - var workItem = await queue.DequeueAsync(TimeSpan.Zero); - Assert.NotNull(workItem); - Assert.Equal("Hello", workItem.Value.Data); - Assert.Equal(1, (await queue.GetQueueStatsAsync()).Dequeued); + Assert.Equal(1, (await queue.GetQueueStatsAsync()).Enqueued); - await workItem.CompleteAsync(); - await Assert.ThrowsAnyAsync(() => workItem.CompleteAsync()); - await Assert.ThrowsAnyAsync(() => workItem.AbandonAsync()); - await Assert.ThrowsAnyAsync(() => workItem.AbandonAsync()); + var workItem = await queue.DequeueAsync(TimeSpan.Zero); + Assert.NotNull(workItem); + Assert.Equal("Hello", workItem.Value.Data); + Assert.Equal(1, (await queue.GetQueueStatsAsync()).Dequeued); - if (_assertStats) - { - var stats = await queue.GetQueueStatsAsync(); - Assert.Equal(0, stats.Abandoned); - Assert.Equal(1, stats.Completed); - Assert.Equal(0, stats.Deadletter); - Assert.Equal(1, stats.Dequeued); - Assert.Equal(1, stats.Enqueued); - Assert.Equal(0, stats.Errors); - Assert.Equal(0, stats.Queued); - Assert.Equal(0, stats.Timeouts); - Assert.Equal(0, stats.Working); - } + await workItem.CompleteAsync(); + await Assert.ThrowsAnyAsync(() => workItem.CompleteAsync()); + await Assert.ThrowsAnyAsync(() => workItem.AbandonAsync()); + await Assert.ThrowsAnyAsync(() => workItem.AbandonAsync()); - if (workItem is QueueEntry queueEntry) - Assert.Equal(1, queueEntry.Attempts); - } - finally + if (_assertStats) { - await CleanupQueueAsync(queue); + var stats = await queue.GetQueueStatsAsync(); + Assert.Equal(0, stats.Abandoned); + Assert.Equal(1, stats.Completed); + Assert.Equal(0, stats.Deadletter); + Assert.Equal(1, stats.Dequeued); + Assert.Equal(1, stats.Enqueued); + Assert.Equal(0, stats.Errors); + Assert.Equal(0, stats.Queued); + Assert.Equal(0, stats.Timeouts); + Assert.Equal(0, stats.Working); } - } - public virtual async Task CanDequeueWithLockingAsync() + if (workItem is QueueEntry queueEntry) + Assert.Equal(1, queueEntry.Attempts); + } + finally { - using var cache = new InMemoryCacheClient(new InMemoryCacheClientOptions { LoggerFactory = Log }); - using var messageBus = new InMemoryMessageBus(new InMemoryMessageBusOptions { LoggerFactory = Log }); - - var distributedLock = new CacheLockProvider(cache, messageBus, Log); - await CanDequeueWithLockingImpAsync(distributedLock); + await CleanupQueueAsync(queue); } + } + + public virtual async Task CanDequeueWithLockingAsync() + { + using var cache = new InMemoryCacheClient(new InMemoryCacheClientOptions { LoggerFactory = Log }); + using var messageBus = new InMemoryMessageBus(new InMemoryMessageBusOptions { LoggerFactory = Log }); + + var distributedLock = new CacheLockProvider(cache, messageBus, Log); + await CanDequeueWithLockingImpAsync(distributedLock); + } + + protected async Task CanDequeueWithLockingImpAsync(CacheLockProvider distributedLock) + { + var queue = GetQueue(retryDelay: TimeSpan.Zero, retries: 0); + if (queue == null) + return; - protected async Task CanDequeueWithLockingImpAsync(CacheLockProvider distributedLock) + try { - var queue = GetQueue(retryDelay: TimeSpan.Zero, retries: 0); - if (queue == null) - return; + await queue.DeleteQueueAsync(); + await AssertEmptyQueueAsync(queue); - try - { - await queue.DeleteQueueAsync(); - await AssertEmptyQueueAsync(queue); + Log.MinimumLevel = LogLevel.Trace; + using var metrics = new InMemoryMetricsClient(new InMemoryMetricsClientOptions { Buffered = false, LoggerFactory = Log }); - Log.MinimumLevel = LogLevel.Trace; - using var metrics = new InMemoryMetricsClient(new InMemoryMetricsClientOptions { Buffered = false, LoggerFactory = Log }); + queue.AttachBehavior(new MetricsQueueBehavior(metrics, loggerFactory: Log)); - queue.AttachBehavior(new MetricsQueueBehavior(metrics, loggerFactory: Log)); + var resetEvent = new AsyncAutoResetEvent(); + await queue.StartWorkingAsync(async w => + { + _logger.LogInformation("Acquiring distributed lock in work item"); + var l = await distributedLock.AcquireAsync("test"); + Assert.NotNull(l); + _logger.LogInformation("Acquired distributed lock"); + await SystemClock.SleepAsync(TimeSpan.FromMilliseconds(250)); + await l.ReleaseAsync(); + _logger.LogInformation("Released distributed lock"); - var resetEvent = new AsyncAutoResetEvent(); - await queue.StartWorkingAsync(async w => - { - _logger.LogInformation("Acquiring distributed lock in work item"); - var l = await distributedLock.AcquireAsync("test"); - Assert.NotNull(l); - _logger.LogInformation("Acquired distributed lock"); - await SystemClock.SleepAsync(TimeSpan.FromMilliseconds(250)); - await l.ReleaseAsync(); - _logger.LogInformation("Released distributed lock"); - - await w.CompleteAsync(); - resetEvent.Set(); - }); + await w.CompleteAsync(); + resetEvent.Set(); + }); - await queue.EnqueueAsync(new SimpleWorkItem { Data = "Hello" }); - await resetEvent.WaitAsync(TimeSpan.FromSeconds(5)); + await queue.EnqueueAsync(new SimpleWorkItem { Data = "Hello" }); + await resetEvent.WaitAsync(TimeSpan.FromSeconds(5)); - if (_assertStats) - { - await SystemClock.SleepAsync(1); - var stats = await queue.GetQueueStatsAsync(); - _logger.LogInformation("Completed: {Completed} Errors: {Errors} Deadletter: {Deadletter} Working: {Working} ", stats.Completed, stats.Errors, stats.Deadletter, stats.Working); - Assert.Equal(1, stats.Completed); - } - } - finally + if (_assertStats) { - await CleanupQueueAsync(queue); + await SystemClock.SleepAsync(1); + var stats = await queue.GetQueueStatsAsync(); + _logger.LogInformation("Completed: {Completed} Errors: {Errors} Deadletter: {Deadletter} Working: {Working} ", stats.Completed, stats.Errors, stats.Deadletter, stats.Working); + Assert.Equal(1, stats.Completed); } } - - public virtual async Task CanHaveMultipleQueueInstancesWithLockingAsync() + finally { - using var cache = new InMemoryCacheClient(new InMemoryCacheClientOptions { LoggerFactory = Log }); - using var messageBus = new InMemoryMessageBus(new InMemoryMessageBusOptions { LoggerFactory = Log }); - - var distributedLock = new CacheLockProvider(cache, messageBus, Log); - await CanHaveMultipleQueueInstancesWithLockingImplAsync(distributedLock); + await CleanupQueueAsync(queue); } + } + + public virtual async Task CanHaveMultipleQueueInstancesWithLockingAsync() + { + using var cache = new InMemoryCacheClient(new InMemoryCacheClientOptions { LoggerFactory = Log }); + using var messageBus = new InMemoryMessageBus(new InMemoryMessageBusOptions { LoggerFactory = Log }); + + var distributedLock = new CacheLockProvider(cache, messageBus, Log); + await CanHaveMultipleQueueInstancesWithLockingImplAsync(distributedLock); + } + + protected async Task CanHaveMultipleQueueInstancesWithLockingImplAsync(CacheLockProvider distributedLock) + { + var queue = GetQueue(retries: 0, retryDelay: TimeSpan.Zero); + if (queue == null) + return; - protected async Task CanHaveMultipleQueueInstancesWithLockingImplAsync(CacheLockProvider distributedLock) + try { - var queue = GetQueue(retries: 0, retryDelay: TimeSpan.Zero); - if (queue == null) - return; + await queue.DeleteQueueAsync(); + await AssertEmptyQueueAsync(queue); + + const int workItemCount = 16; + const int workerCount = 4; + var countdown = new AsyncCountdownEvent(workItemCount); + var info = new WorkInfo(); + var workers = new List> { queue }; try { - await queue.DeleteQueueAsync(); - await AssertEmptyQueueAsync(queue); - - const int workItemCount = 16; - const int workerCount = 4; - var countdown = new AsyncCountdownEvent(workItemCount); - var info = new WorkInfo(); - var workers = new List> { queue }; - - try + for (int i = 0; i < workerCount; i++) { - for (int i = 0; i < workerCount; i++) + var q = GetQueue(retries: 0, retryDelay: TimeSpan.Zero); + int instanceCount = i; + await q.StartWorkingAsync(async w => { - var q = GetQueue(retries: 0, retryDelay: TimeSpan.Zero); - int instanceCount = i; - await q.StartWorkingAsync(async w => - { - if (_logger.IsEnabled(LogLevel.Information)) - _logger.LogInformation("[{Instance}] Acquiring distributed lock in work item: {Id}", instanceCount, w.Id); - var l = await distributedLock.AcquireAsync("test"); - Assert.NotNull(l); - if (_logger.IsEnabled(LogLevel.Information)) - _logger.LogInformation("[{Instance}] Acquired distributed lock: {Id}", instanceCount, w.Id); - await SystemClock.SleepAsync(TimeSpan.FromMilliseconds(50)); - await l.ReleaseAsync(); - if (_logger.IsEnabled(LogLevel.Information)) - _logger.LogInformation("[{Instance}] Released distributed lock: {Id}", instanceCount, w.Id); - - await w.CompleteAsync(); - info.IncrementCompletedCount(); - countdown.Signal(); - if (_logger.IsEnabled(LogLevel.Information)) - _logger.LogInformation("[{Instance}] Signaled countdown: {Id}", instanceCount, w.Id); - }); - workers.Add(q); - } + if (_logger.IsEnabled(LogLevel.Information)) + _logger.LogInformation("[{Instance}] Acquiring distributed lock in work item: {Id}", instanceCount, w.Id); + var l = await distributedLock.AcquireAsync("test"); + Assert.NotNull(l); + if (_logger.IsEnabled(LogLevel.Information)) + _logger.LogInformation("[{Instance}] Acquired distributed lock: {Id}", instanceCount, w.Id); + await SystemClock.SleepAsync(TimeSpan.FromMilliseconds(50)); + await l.ReleaseAsync(); + if (_logger.IsEnabled(LogLevel.Information)) + _logger.LogInformation("[{Instance}] Released distributed lock: {Id}", instanceCount, w.Id); + + await w.CompleteAsync(); + info.IncrementCompletedCount(); + countdown.Signal(); + if (_logger.IsEnabled(LogLevel.Information)) + _logger.LogInformation("[{Instance}] Signaled countdown: {Id}", instanceCount, w.Id); + }); + workers.Add(q); + } - await Run.InParallelAsync(workItemCount, async i => + await Run.InParallelAsync(workItemCount, async i => + { + string id = await queue.EnqueueAsync(new SimpleWorkItem { - string id = await queue.EnqueueAsync(new SimpleWorkItem - { - Data = "Hello", - Id = i - }); - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Enqueued Index: {Instance} Id: {Id}", i, id); + Data = "Hello", + Id = i }); + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Enqueued Index: {Instance} Id: {Id}", i, id); + }); - await countdown.WaitAsync(TimeSpan.FromSeconds(5)); - await SystemClock.SleepAsync(50); - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Completed: {Completed} Abandoned: {Abandoned} Error: {Errors}", info.CompletedCount, info.AbandonCount, info.ErrorCount); + await countdown.WaitAsync(TimeSpan.FromSeconds(5)); + await SystemClock.SleepAsync(50); + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Completed: {Completed} Abandoned: {Abandoned} Error: {Errors}", info.CompletedCount, info.AbandonCount, info.ErrorCount); - if (_logger.IsEnabled(LogLevel.Information)) - _logger.LogInformation("Work Info Stats: Completed: {Completed} Abandoned: {Abandoned} Error: {Errors}", info.CompletedCount, info.AbandonCount, info.ErrorCount); - Assert.Equal(workItemCount, info.CompletedCount + info.AbandonCount + info.ErrorCount); + if (_logger.IsEnabled(LogLevel.Information)) + _logger.LogInformation("Work Info Stats: Completed: {Completed} Abandoned: {Abandoned} Error: {Errors}", info.CompletedCount, info.AbandonCount, info.ErrorCount); + Assert.Equal(workItemCount, info.CompletedCount + info.AbandonCount + info.ErrorCount); - // In memory queue doesn't share state. - if (queue.GetType() == typeof(InMemoryQueue)) - { - var stats = await queue.GetQueueStatsAsync(); - Assert.Equal(info.CompletedCount, stats.Completed); - } - else - { - var workerStats = new List(); - for (int i = 0; i < workers.Count; i++) - { - var stats = await workers[i].GetQueueStatsAsync(); - if (_logger.IsEnabled(LogLevel.Information)) - _logger.LogInformation("Worker#{Id} Working: {Working} Completed: {Completed} Abandoned: {Abandoned} Error: {Errors} Deadletter: {Deadletter}", i, stats.Working, stats.Completed, stats.Abandoned, stats.Errors, stats.Deadletter); - workerStats.Add(stats); - } - - Assert.Equal(info.CompletedCount, workerStats.Sum(s => s.Completed)); - } + // In memory queue doesn't share state. + if (queue.GetType() == typeof(InMemoryQueue)) + { + var stats = await queue.GetQueueStatsAsync(); + Assert.Equal(info.CompletedCount, stats.Completed); } - finally + else { - foreach (var q in workers) - await CleanupQueueAsync(q); + var workerStats = new List(); + for (int i = 0; i < workers.Count; i++) + { + var stats = await workers[i].GetQueueStatsAsync(); + if (_logger.IsEnabled(LogLevel.Information)) + _logger.LogInformation("Worker#{Id} Working: {Working} Completed: {Completed} Abandoned: {Abandoned} Error: {Errors} Deadletter: {Deadletter}", i, stats.Working, stats.Completed, stats.Abandoned, stats.Errors, stats.Deadletter); + workerStats.Add(stats); + } + + Assert.Equal(info.CompletedCount, workerStats.Sum(s => s.Completed)); } } finally { - await CleanupQueueAsync(queue); + foreach (var q in workers) + await CleanupQueueAsync(q); } } - - protected async Task DoWorkAsync(IQueueEntry w, AsyncCountdownEvent countdown, WorkInfo info) + finally { - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Starting: {Id}", w.Value.Id); - Assert.Equal("Hello", w.Value.Data); + await CleanupQueueAsync(queue); + } + } - try + protected async Task DoWorkAsync(IQueueEntry w, AsyncCountdownEvent countdown, WorkInfo info) + { + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Starting: {Id}", w.Value.Id); + Assert.Equal("Hello", w.Value.Data); + + try + { + // randomly complete, abandon or blowup. + if (RandomData.GetBool()) { - // randomly complete, abandon or blowup. - if (RandomData.GetBool()) - { - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Completing: {Id}", w.Value.Id); - await w.CompleteAsync(); - info.IncrementCompletedCount(); - } - else if (RandomData.GetBool()) - { - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Abandoning: {Id}", w.Value.Id); - await w.AbandonAsync(); - info.IncrementAbandonCount(); - } - else - { - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Erroring: {Id}", w.Value.Id); - info.IncrementErrorCount(); - throw new Exception(); - } + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Completing: {Id}", w.Value.Id); + await w.CompleteAsync(); + info.IncrementCompletedCount(); } - finally + else if (RandomData.GetBool()) { - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Signal {CurrentCount}", countdown.CurrentCount); - countdown.Signal(); + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Abandoning: {Id}", w.Value.Id); + await w.AbandonAsync(); + info.IncrementAbandonCount(); + } + else + { + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Erroring: {Id}", w.Value.Id); + info.IncrementErrorCount(); + throw new Exception(); } } + finally + { + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Signal {CurrentCount}", countdown.CurrentCount); + countdown.Signal(); + } + } + + protected async Task AssertEmptyQueueAsync(IQueue queue) + { + if (_assertStats) + { + var stats = await queue.GetQueueStatsAsync(); + Assert.Equal(0, stats.Abandoned); + Assert.Equal(0, stats.Completed); + Assert.Equal(0, stats.Deadletter); + Assert.Equal(0, stats.Dequeued); + Assert.Equal(0, stats.Enqueued); + Assert.Equal(0, stats.Errors); + Assert.Equal(0, stats.Queued); + Assert.Equal(0, stats.Timeouts); + Assert.Equal(0, stats.Working); + } + } - protected async Task AssertEmptyQueueAsync(IQueue queue) + public virtual async Task MaintainJobNotAbandon_NotWorkTimeOutEntry() + { + var queue = GetQueue(retries: 0, workItemTimeout: TimeSpan.FromMilliseconds(100), retryDelay: TimeSpan.Zero); + if (queue == null) + return; + try { + await queue.DeleteQueueAsync(); + await AssertEmptyQueueAsync(queue); + queue.EnqueueAsync(new SimpleWorkItem + { + Data = "Hello World", + Id = 1 + }); + queue.EnqueueAsync(new SimpleWorkItem + { + Data = "Hello World", + Id = 2 + }); + + var dequeuedQueueItem = Assert.IsType>(await queue.DequeueAsync()); + Assert.NotNull(dequeuedQueueItem.Value); + // The first dequeued item works for 60 milliseconds less than work timeout(100 milliseconds). + await SystemClock.SleepAsync(60); + await dequeuedQueueItem.CompleteAsync(); + Assert.True(dequeuedQueueItem.IsCompleted); + Assert.False(dequeuedQueueItem.IsAbandoned); + + dequeuedQueueItem = Assert.IsType>(await queue.DequeueAsync()); + Assert.NotNull(dequeuedQueueItem.Value); + // The second dequeued item works for 60 milliseconds less than work timeout(100 milliseconds). + await SystemClock.SleepAsync(60); + await dequeuedQueueItem.CompleteAsync(); + Assert.True(dequeuedQueueItem.IsCompleted); + Assert.False(dequeuedQueueItem.IsAbandoned); + if (_assertStats) { var stats = await queue.GetQueueStatsAsync(); - Assert.Equal(0, stats.Abandoned); - Assert.Equal(0, stats.Completed); - Assert.Equal(0, stats.Deadletter); - Assert.Equal(0, stats.Dequeued); - Assert.Equal(0, stats.Enqueued); - Assert.Equal(0, stats.Errors); - Assert.Equal(0, stats.Queued); - Assert.Equal(0, stats.Timeouts); Assert.Equal(0, stats.Working); + Assert.Equal(0, stats.Abandoned); + Assert.Equal(2, stats.Completed); } } + finally + { + await CleanupQueueAsync(queue); + } + } + + public virtual async Task CanHandleAutoAbandonInWorker() + { + // create queue with short work item timeout so it will be auto abandoned + var queue = GetQueue(workItemTimeout: TimeSpan.FromMilliseconds(100)); + if (queue == null) + return; - public virtual async Task MaintainJobNotAbandon_NotWorkTimeOutEntry() + try { - var queue = GetQueue(retries: 0, workItemTimeout: TimeSpan.FromMilliseconds(100), retryDelay: TimeSpan.Zero); - if (queue == null) - return; - try - { - await queue.DeleteQueueAsync(); - await AssertEmptyQueueAsync(queue); - queue.EnqueueAsync(new SimpleWorkItem - { - Data = "Hello World", - Id = 1 - }); - queue.EnqueueAsync(new SimpleWorkItem - { - Data = "Hello World", - Id = 2 - }); + await queue.DeleteQueueAsync(); - var dequeuedQueueItem = Assert.IsType>(await queue.DequeueAsync()); - Assert.NotNull(dequeuedQueueItem.Value); - // The first dequeued item works for 60 milliseconds less than work timeout(100 milliseconds). - await SystemClock.SleepAsync(60); - await dequeuedQueueItem.CompleteAsync(); - Assert.True(dequeuedQueueItem.IsCompleted); - Assert.False(dequeuedQueueItem.IsAbandoned); - - dequeuedQueueItem = Assert.IsType>(await queue.DequeueAsync()); - Assert.NotNull(dequeuedQueueItem.Value); - // The second dequeued item works for 60 milliseconds less than work timeout(100 milliseconds). - await SystemClock.SleepAsync(60); - await dequeuedQueueItem.CompleteAsync(); - Assert.True(dequeuedQueueItem.IsCompleted); - Assert.False(dequeuedQueueItem.IsAbandoned); + var successEvent = new AsyncAutoResetEvent(); + var errorEvent = new AsyncAutoResetEvent(); - if (_assertStats) + await queue.StartWorkingAsync(async (item) => + { + if (item.Value.Data == "Delay") { + // wait for queue item to get auto abandoned var stats = await queue.GetQueueStatsAsync(); - Assert.Equal(0, stats.Working); - Assert.Equal(0, stats.Abandoned); - Assert.Equal(2, stats.Completed); - } - } - finally - { - await CleanupQueueAsync(queue); - } - } - - public virtual async Task CanHandleAutoAbandonInWorker() - { - // create queue with short work item timeout so it will be auto abandoned - var queue = GetQueue(workItemTimeout: TimeSpan.FromMilliseconds(100)); - if (queue == null) - return; + var sw = Stopwatch.StartNew(); + do + { + if (stats.Abandoned > 0) + break; - try - { - await queue.DeleteQueueAsync(); + stats = await queue.GetQueueStatsAsync(); + } while (sw.Elapsed < TimeSpan.FromSeconds(10)); - var successEvent = new AsyncAutoResetEvent(); - var errorEvent = new AsyncAutoResetEvent(); + Assert.Equal(1, stats.Abandoned); + } - await queue.StartWorkingAsync(async (item) => + try { - if (item.Value.Data == "Delay") - { - // wait for queue item to get auto abandoned - var stats = await queue.GetQueueStatsAsync(); - var sw = Stopwatch.StartNew(); - do - { - if (stats.Abandoned > 0) - break; - - stats = await queue.GetQueueStatsAsync(); - } while (sw.Elapsed < TimeSpan.FromSeconds(10)); - - Assert.Equal(1, stats.Abandoned); - } - - try - { - await item.CompleteAsync(); - } - catch - { - errorEvent.Set(); - throw; - } + await item.CompleteAsync(); + } + catch + { + errorEvent.Set(); + throw; + } - successEvent.Set(); - }); + successEvent.Set(); + }); - await queue.EnqueueAsync(new SimpleWorkItem() { Data = "Delay" }); - await queue.EnqueueAsync(new SimpleWorkItem() { Data = "No Delay" }); + await queue.EnqueueAsync(new SimpleWorkItem() { Data = "Delay" }); + await queue.EnqueueAsync(new SimpleWorkItem() { Data = "No Delay" }); - await errorEvent.WaitAsync(TimeSpan.FromSeconds(10)); - await successEvent.WaitAsync(TimeSpan.FromSeconds(10)); - } - finally - { - await CleanupQueueAsync(queue); - } + await errorEvent.WaitAsync(TimeSpan.FromSeconds(10)); + await successEvent.WaitAsync(TimeSpan.FromSeconds(10)); } - - public virtual void Dispose() + finally { - var queue = GetQueue(); - if (queue == null) - return; - - using (queue) - _ = Task.Run(queue.DeleteQueueAsync); - - GC.SuppressFinalize(this); + await CleanupQueueAsync(queue); } } - public class WorkInfo + public virtual void Dispose() { - private int _abandonCount; - private int _errorCount; - private int _completedCount; + var queue = GetQueue(); + if (queue == null) + return; - public int AbandonCount => _abandonCount; - public int ErrorCount => _errorCount; - public int CompletedCount => _completedCount; + using (queue) + _ = Task.Run(queue.DeleteQueueAsync); - public void IncrementAbandonCount() - { - Interlocked.Increment(ref _abandonCount); - } + GC.SuppressFinalize(this); + } +} - public void IncrementErrorCount() - { - Interlocked.Increment(ref _errorCount); - } +public class WorkInfo +{ + private int _abandonCount; + private int _errorCount; + private int _completedCount; - public void IncrementCompletedCount() - { - Interlocked.Increment(ref _completedCount); - } + public int AbandonCount => _abandonCount; + public int ErrorCount => _errorCount; + public int CompletedCount => _completedCount; + + public void IncrementAbandonCount() + { + Interlocked.Increment(ref _abandonCount); + } + + public void IncrementErrorCount() + { + Interlocked.Increment(ref _errorCount); + } + + public void IncrementCompletedCount() + { + Interlocked.Increment(ref _completedCount); } } diff --git a/src/Foundatio.TestHarness/Queue/Samples.cs b/src/Foundatio.TestHarness/Queue/Samples.cs index 81a0757d1..1ced5d51e 100644 --- a/src/Foundatio.TestHarness/Queue/Samples.cs +++ b/src/Foundatio.TestHarness/Queue/Samples.cs @@ -1,13 +1,12 @@ using Foundatio.Metrics; using Foundatio.Queues; -namespace Foundatio.Tests.Queue +namespace Foundatio.Tests.Queue; + +public class SimpleWorkItem : IHaveSubMetricName, IHaveUniqueIdentifier { - public class SimpleWorkItem : IHaveSubMetricName, IHaveUniqueIdentifier - { - public string Data { get; set; } - public int Id { get; set; } - public string UniqueIdentifier { get; set; } - public string SubMetricName { get; set; } - } + public string Data { get; set; } + public int Id { get; set; } + public string UniqueIdentifier { get; set; } + public string SubMetricName { get; set; } } diff --git a/src/Foundatio.TestHarness/Serializer/SerializerTestsBase.cs b/src/Foundatio.TestHarness/Serializer/SerializerTestsBase.cs index 707cfe685..85e9651d6 100644 --- a/src/Foundatio.TestHarness/Serializer/SerializerTestsBase.cs +++ b/src/Foundatio.TestHarness/Serializer/SerializerTestsBase.cs @@ -7,90 +7,53 @@ using Xunit; using Xunit.Abstractions; -namespace Foundatio.Tests.Serializer +namespace Foundatio.Tests.Serializer; + +public abstract class SerializerTestsBase : TestWithLoggingBase { - public abstract class SerializerTestsBase : TestWithLoggingBase + protected SerializerTestsBase(ITestOutputHelper output) : base(output) { } + + protected virtual ISerializer GetSerializer() { - protected SerializerTestsBase(ITestOutputHelper output) : base(output) { } + return null; + } - protected virtual ISerializer GetSerializer() - { - return null; - } + public virtual void CanRoundTripBytes() + { + var serializer = GetSerializer(); + if (serializer == null) + return; - public virtual void CanRoundTripBytes() + var model = new SerializeModel { - var serializer = GetSerializer(); - if (serializer == null) - return; - - var model = new SerializeModel - { - IntProperty = 1, - StringProperty = "test", - ListProperty = new List { 1 }, - ObjectProperty = new SerializeModel { IntProperty = 1 } - }; - - byte[] bytes = serializer.SerializeToBytes(model); - var actual = serializer.Deserialize(bytes); - Assert.Equal(model.IntProperty, actual.IntProperty); - Assert.Equal(model.StringProperty, actual.StringProperty); - Assert.Equal(model.ListProperty, actual.ListProperty); - - string text = serializer.SerializeToString(model); - actual = serializer.Deserialize(text); - Assert.Equal(model.IntProperty, actual.IntProperty); - Assert.Equal(model.StringProperty, actual.StringProperty); - Assert.Equal(model.ListProperty, actual.ListProperty); - Assert.NotNull(model.ObjectProperty); - Assert.Equal(1, ((dynamic)model.ObjectProperty).IntProperty); - } - - public virtual void CanRoundTripString() - { - var serializer = GetSerializer(); - if (serializer == null) - return; - - var model = new SerializeModel - { - IntProperty = 1, - StringProperty = "test", - ListProperty = new List { 1 }, - ObjectProperty = new SerializeModel { IntProperty = 1 } - }; - - string text = serializer.SerializeToString(model); - _logger.LogInformation(text); - var actual = serializer.Deserialize(text); - Assert.Equal(model.IntProperty, actual.IntProperty); - Assert.Equal(model.StringProperty, actual.StringProperty); - Assert.Equal(model.ListProperty, actual.ListProperty); - Assert.NotNull(model.ObjectProperty); - Assert.Equal(1, ((dynamic)model.ObjectProperty).IntProperty); - } - - public virtual void CanHandlePrimitiveTypes() - { - var serializer = GetSerializer(); - if (serializer == null) - return; - - object expected = "primitive"; - string text = serializer.SerializeToString(expected); - _logger.LogInformation(text); - var actual = serializer.Deserialize(text); - Assert.Equal(expected, actual); - } + IntProperty = 1, + StringProperty = "test", + ListProperty = new List { 1 }, + ObjectProperty = new SerializeModel { IntProperty = 1 } + }; + + byte[] bytes = serializer.SerializeToBytes(model); + var actual = serializer.Deserialize(bytes); + Assert.Equal(model.IntProperty, actual.IntProperty); + Assert.Equal(model.StringProperty, actual.StringProperty); + Assert.Equal(model.ListProperty, actual.ListProperty); + + string text = serializer.SerializeToString(model); + actual = serializer.Deserialize(text); + Assert.Equal(model.IntProperty, actual.IntProperty); + Assert.Equal(model.StringProperty, actual.StringProperty); + Assert.Equal(model.ListProperty, actual.ListProperty); + Assert.NotNull(model.ObjectProperty); + Assert.Equal(1, ((dynamic)model.ObjectProperty).IntProperty); } - [MemoryDiagnoser] - [ShortRunJob] - public abstract class SerializerBenchmarkBase + public virtual void CanRoundTripString() { - private ISerializer _serializer; - private readonly SerializeModel _data = new() + var serializer = GetSerializer(); + if (serializer == null) + return; + + var model = new SerializeModel { IntProperty = 1, StringProperty = "test", @@ -98,42 +61,78 @@ public abstract class SerializerBenchmarkBase ObjectProperty = new SerializeModel { IntProperty = 1 } }; - private byte[] _serializedData; + string text = serializer.SerializeToString(model); + _logger.LogInformation(text); + var actual = serializer.Deserialize(text); + Assert.Equal(model.IntProperty, actual.IntProperty); + Assert.Equal(model.StringProperty, actual.StringProperty); + Assert.Equal(model.ListProperty, actual.ListProperty); + Assert.NotNull(model.ObjectProperty); + Assert.Equal(1, ((dynamic)model.ObjectProperty).IntProperty); + } + + public virtual void CanHandlePrimitiveTypes() + { + var serializer = GetSerializer(); + if (serializer == null) + return; + + object expected = "primitive"; + string text = serializer.SerializeToString(expected); + _logger.LogInformation(text); + var actual = serializer.Deserialize(text); + Assert.Equal(expected, actual); + } +} + +[MemoryDiagnoser] +[ShortRunJob] +public abstract class SerializerBenchmarkBase +{ + private ISerializer _serializer; + private readonly SerializeModel _data = new() + { + IntProperty = 1, + StringProperty = "test", + ListProperty = new List { 1 }, + ObjectProperty = new SerializeModel { IntProperty = 1 } + }; - protected abstract ISerializer GetSerializer(); + private byte[] _serializedData; - [GlobalSetup] - public void Setup() - { - _serializer = GetSerializer(); - _serializedData = _serializer.SerializeToBytes(_data); - } + protected abstract ISerializer GetSerializer(); - [Benchmark] - public byte[] Serialize() - { - return _serializer.SerializeToBytes(_data); - } + [GlobalSetup] + public void Setup() + { + _serializer = GetSerializer(); + _serializedData = _serializer.SerializeToBytes(_data); + } - [Benchmark] - public SerializeModel Deserialize() - { - return _serializer.Deserialize(_serializedData); - } + [Benchmark] + public byte[] Serialize() + { + return _serializer.SerializeToBytes(_data); + } - [Benchmark] - public SerializeModel RoundTrip() - { - byte[] serializedData = _serializer.SerializeToBytes(_data); - return _serializer.Deserialize(serializedData); - } + [Benchmark] + public SerializeModel Deserialize() + { + return _serializer.Deserialize(_serializedData); } - public class SerializeModel + [Benchmark] + public SerializeModel RoundTrip() { - public int IntProperty { get; set; } - public string StringProperty { get; set; } - public List ListProperty { get; set; } - public object ObjectProperty { get; set; } + byte[] serializedData = _serializer.SerializeToBytes(_data); + return _serializer.Deserialize(serializedData); } } + +public class SerializeModel +{ + public int IntProperty { get; set; } + public string StringProperty { get; set; } + public List ListProperty { get; set; } + public object ObjectProperty { get; set; } +} diff --git a/src/Foundatio.TestHarness/Storage/FileStorageTestsBase.cs b/src/Foundatio.TestHarness/Storage/FileStorageTestsBase.cs index 0791a0ae6..4adb39293 100644 --- a/src/Foundatio.TestHarness/Storage/FileStorageTestsBase.cs +++ b/src/Foundatio.TestHarness/Storage/FileStorageTestsBase.cs @@ -14,678 +14,677 @@ using Xunit; using Xunit.Abstractions; -namespace Foundatio.Tests.Storage +namespace Foundatio.Tests.Storage; + +public abstract class FileStorageTestsBase : TestWithLoggingBase { - public abstract class FileStorageTestsBase : TestWithLoggingBase - { - protected FileStorageTestsBase(ITestOutputHelper output) : base(output) { } + protected FileStorageTestsBase(ITestOutputHelper output) : base(output) { } - protected virtual IFileStorage GetStorage() - { - return null; - } + protected virtual IFileStorage GetStorage() + { + return null; + } - public virtual async Task CanGetEmptyFileListOnMissingDirectoryAsync() - { - var storage = GetStorage(); - if (storage == null) - return; + public virtual async Task CanGetEmptyFileListOnMissingDirectoryAsync() + { + var storage = GetStorage(); + if (storage == null) + return; - await ResetAsync(storage); + await ResetAsync(storage); - using (storage) - { - Assert.Empty(await storage.GetFileListAsync(Guid.NewGuid() + "\\*")); - } + using (storage) + { + Assert.Empty(await storage.GetFileListAsync(Guid.NewGuid() + "\\*")); } + } - public virtual async Task CanGetFileListForSingleFolderAsync() - { - var storage = GetStorage(); - if (storage == null) - return; + public virtual async Task CanGetFileListForSingleFolderAsync() + { + var storage = GetStorage(); + if (storage == null) + return; - await ResetAsync(storage); + await ResetAsync(storage); - using (storage) - { - await storage.SaveFileAsync(@"archived\archived.txt", "archived"); - await storage.SaveFileAsync(@"q\new.txt", "new"); - await storage.SaveFileAsync(@"long/path/in/here/1.hey.stuff-2.json", "archived"); + using (storage) + { + await storage.SaveFileAsync(@"archived\archived.txt", "archived"); + await storage.SaveFileAsync(@"q\new.txt", "new"); + await storage.SaveFileAsync(@"long/path/in/here/1.hey.stuff-2.json", "archived"); - Assert.Equal(3, (await storage.GetFileListAsync()).Count); - Assert.Single(await storage.GetFileListAsync(limit: 1)); - Assert.Single(await storage.GetFileListAsync(@"long\path\in\here\*stuff*.json")); + Assert.Equal(3, (await storage.GetFileListAsync()).Count); + Assert.Single(await storage.GetFileListAsync(limit: 1)); + Assert.Single(await storage.GetFileListAsync(@"long\path\in\here\*stuff*.json")); - Assert.Single(await storage.GetFileListAsync(@"archived\*")); - Assert.Equal("archived", await storage.GetFileContentsAsync(@"archived\archived.txt")); + Assert.Single(await storage.GetFileListAsync(@"archived\*")); + Assert.Equal("archived", await storage.GetFileContentsAsync(@"archived\archived.txt")); - Assert.Single(await storage.GetFileListAsync(@"q\*")); - Assert.Equal("new", await storage.GetFileContentsAsync(@"q\new.txt")); - } + Assert.Single(await storage.GetFileListAsync(@"q\*")); + Assert.Equal("new", await storage.GetFileContentsAsync(@"q\new.txt")); } + } - public virtual async Task CanGetFileListForSingleFileAsync() - { - var storage = GetStorage(); - if (storage == null) - return; + public virtual async Task CanGetFileListForSingleFileAsync() + { + var storage = GetStorage(); + if (storage == null) + return; - await ResetAsync(storage); + await ResetAsync(storage); - using (storage) - { - await storage.SaveFileAsync(@"archived\archived.txt", "archived"); - await storage.SaveFileAsync(@"archived\archived.csv", "archived"); - await storage.SaveFileAsync(@"q\new.txt", "new"); - await storage.SaveFileAsync(@"long/path/in/here/1.hey.stuff-2.json", "archived"); + using (storage) + { + await storage.SaveFileAsync(@"archived\archived.txt", "archived"); + await storage.SaveFileAsync(@"archived\archived.csv", "archived"); + await storage.SaveFileAsync(@"q\new.txt", "new"); + await storage.SaveFileAsync(@"long/path/in/here/1.hey.stuff-2.json", "archived"); - Assert.Single(await storage.GetFileListAsync(@"archived\archived.txt")); - } + Assert.Single(await storage.GetFileListAsync(@"archived\archived.txt")); } + } - public virtual async Task CanGetPagedFileListForSingleFolderAsync() - { - var storage = GetStorage(); - if (storage == null) - return; + public virtual async Task CanGetPagedFileListForSingleFolderAsync() + { + var storage = GetStorage(); + if (storage == null) + return; - await ResetAsync(storage); + await ResetAsync(storage); - using (storage) - { - var result = await storage.GetPagedFileListAsync(1); - Assert.False(result.HasMore); - Assert.Empty(result.Files); - Assert.False(await result.NextPageAsync()); - Assert.False(result.HasMore); - Assert.Empty(result.Files); - - await storage.SaveFileAsync(@"archived\archived.txt", "archived"); - result = await storage.GetPagedFileListAsync(1); - Assert.False(result.HasMore); - Assert.Single(result.Files); - Assert.False(await result.NextPageAsync()); - Assert.False(result.HasMore); - Assert.Single(result.Files); - - await storage.SaveFileAsync(@"q\new.txt", "new"); - result = await storage.GetPagedFileListAsync(1); - Assert.True(result.HasMore); - Assert.Single(result.Files); - Assert.True(await result.NextPageAsync()); - Assert.False(result.HasMore); - Assert.Single(result.Files); - - await storage.SaveFileAsync(@"long/path/in/here/1.hey.stuff-2.json", "archived"); - - Assert.Equal(3, (await storage.GetPagedFileListAsync(100)).Files.Count); - Assert.Single((await storage.GetPagedFileListAsync(1)).Files); - Assert.Single((await storage.GetPagedFileListAsync(2, @"long\path\in\here\*stuff*.json")).Files); - - Assert.Single((await storage.GetPagedFileListAsync(2, @"archived\*")).Files); - Assert.Equal("archived", await storage.GetFileContentsAsync(@"archived\archived.txt")); - - Assert.Single((await storage.GetPagedFileListAsync(2, @"q\*")).Files); - Assert.Equal("new", await storage.GetFileContentsAsync(@"q\new.txt")); - } + using (storage) + { + var result = await storage.GetPagedFileListAsync(1); + Assert.False(result.HasMore); + Assert.Empty(result.Files); + Assert.False(await result.NextPageAsync()); + Assert.False(result.HasMore); + Assert.Empty(result.Files); + + await storage.SaveFileAsync(@"archived\archived.txt", "archived"); + result = await storage.GetPagedFileListAsync(1); + Assert.False(result.HasMore); + Assert.Single(result.Files); + Assert.False(await result.NextPageAsync()); + Assert.False(result.HasMore); + Assert.Single(result.Files); + + await storage.SaveFileAsync(@"q\new.txt", "new"); + result = await storage.GetPagedFileListAsync(1); + Assert.True(result.HasMore); + Assert.Single(result.Files); + Assert.True(await result.NextPageAsync()); + Assert.False(result.HasMore); + Assert.Single(result.Files); + + await storage.SaveFileAsync(@"long/path/in/here/1.hey.stuff-2.json", "archived"); + + Assert.Equal(3, (await storage.GetPagedFileListAsync(100)).Files.Count); + Assert.Single((await storage.GetPagedFileListAsync(1)).Files); + Assert.Single((await storage.GetPagedFileListAsync(2, @"long\path\in\here\*stuff*.json")).Files); + + Assert.Single((await storage.GetPagedFileListAsync(2, @"archived\*")).Files); + Assert.Equal("archived", await storage.GetFileContentsAsync(@"archived\archived.txt")); + + Assert.Single((await storage.GetPagedFileListAsync(2, @"q\*")).Files); + Assert.Equal("new", await storage.GetFileContentsAsync(@"q\new.txt")); } + } - public virtual async Task CanGetFileInfoAsync() - { - var storage = GetStorage(); - if (storage == null) - return; + public virtual async Task CanGetFileInfoAsync() + { + var storage = GetStorage(); + if (storage == null) + return; - await ResetAsync(storage); + await ResetAsync(storage); - using (storage) - { - var fileInfo = await storage.GetFileInfoAsync(Guid.NewGuid().ToString()); - Assert.Null(fileInfo); - - var startTime = SystemClock.UtcNow.Subtract(TimeSpan.FromMinutes(1)); - string path = $"folder\\{Guid.NewGuid()}-nested.txt"; - Assert.True(await storage.SaveFileAsync(path, "test")); - fileInfo = await storage.GetFileInfoAsync(path); - Assert.NotNull(fileInfo); - Assert.True(fileInfo.Path.EndsWith("nested.txt"), "Incorrect file"); - Assert.True(fileInfo.Size > 0, "Incorrect file size"); - Assert.Equal(DateTimeKind.Utc, fileInfo.Created.Kind); - // NOTE: File creation time might not be accurate: http://stackoverflow.com/questions/2109152/unbelievable-strange-file-creation-time-problem - Assert.True(fileInfo.Created > DateTime.MinValue, "File creation time should be newer than the start time"); - Assert.Equal(DateTimeKind.Utc, fileInfo.Modified.Kind); - Assert.True(startTime <= fileInfo.Modified, $"File {path} modified time {fileInfo.Modified:O} should be newer than the start time {startTime:O}."); - - path = $"{Guid.NewGuid()}-test.txt"; - Assert.True(await storage.SaveFileAsync(path, "test")); - fileInfo = await storage.GetFileInfoAsync(path); - Assert.NotNull(fileInfo); - Assert.True(fileInfo.Path.EndsWith("test.txt"), "Incorrect file"); - Assert.True(fileInfo.Size > 0, "Incorrect file size"); - Assert.Equal(DateTimeKind.Utc, fileInfo.Created.Kind); - Assert.True(fileInfo.Created > DateTime.MinValue, "File creation time should be newer than the start time."); - Assert.Equal(DateTimeKind.Utc, fileInfo.Modified.Kind); - Assert.True(startTime <= fileInfo.Modified, $"File {path} modified time {fileInfo.Modified:O} should be newer than the start time {startTime:O}."); - } + using (storage) + { + var fileInfo = await storage.GetFileInfoAsync(Guid.NewGuid().ToString()); + Assert.Null(fileInfo); + + var startTime = SystemClock.UtcNow.Subtract(TimeSpan.FromMinutes(1)); + string path = $"folder\\{Guid.NewGuid()}-nested.txt"; + Assert.True(await storage.SaveFileAsync(path, "test")); + fileInfo = await storage.GetFileInfoAsync(path); + Assert.NotNull(fileInfo); + Assert.True(fileInfo.Path.EndsWith("nested.txt"), "Incorrect file"); + Assert.True(fileInfo.Size > 0, "Incorrect file size"); + Assert.Equal(DateTimeKind.Utc, fileInfo.Created.Kind); + // NOTE: File creation time might not be accurate: http://stackoverflow.com/questions/2109152/unbelievable-strange-file-creation-time-problem + Assert.True(fileInfo.Created > DateTime.MinValue, "File creation time should be newer than the start time"); + Assert.Equal(DateTimeKind.Utc, fileInfo.Modified.Kind); + Assert.True(startTime <= fileInfo.Modified, $"File {path} modified time {fileInfo.Modified:O} should be newer than the start time {startTime:O}."); + + path = $"{Guid.NewGuid()}-test.txt"; + Assert.True(await storage.SaveFileAsync(path, "test")); + fileInfo = await storage.GetFileInfoAsync(path); + Assert.NotNull(fileInfo); + Assert.True(fileInfo.Path.EndsWith("test.txt"), "Incorrect file"); + Assert.True(fileInfo.Size > 0, "Incorrect file size"); + Assert.Equal(DateTimeKind.Utc, fileInfo.Created.Kind); + Assert.True(fileInfo.Created > DateTime.MinValue, "File creation time should be newer than the start time."); + Assert.Equal(DateTimeKind.Utc, fileInfo.Modified.Kind); + Assert.True(startTime <= fileInfo.Modified, $"File {path} modified time {fileInfo.Modified:O} should be newer than the start time {startTime:O}."); } + } - public virtual async Task CanGetNonExistentFileInfoAsync() - { - var storage = GetStorage(); - if (storage == null) - return; + public virtual async Task CanGetNonExistentFileInfoAsync() + { + var storage = GetStorage(); + if (storage == null) + return; - await ResetAsync(storage); + await ResetAsync(storage); - using (storage) - { - await Assert.ThrowsAnyAsync(() => storage.GetFileInfoAsync(null)); - Assert.Null(await storage.GetFileInfoAsync(Guid.NewGuid().ToString())); - } + using (storage) + { + await Assert.ThrowsAnyAsync(() => storage.GetFileInfoAsync(null)); + Assert.Null(await storage.GetFileInfoAsync(Guid.NewGuid().ToString())); } + } - public virtual async Task CanManageFilesAsync() - { - var storage = GetStorage(); - if (storage == null) - return; + public virtual async Task CanManageFilesAsync() + { + var storage = GetStorage(); + if (storage == null) + return; - await ResetAsync(storage); + await ResetAsync(storage); - using (storage) - { - await storage.SaveFileAsync("test.txt", "test"); - var file = (await storage.GetFileListAsync()).Single(); - Assert.NotNull(file); - Assert.Equal("test.txt", file.Path); - string content = await storage.GetFileContentsAsync("test.txt"); - Assert.Equal("test", content); - await storage.RenameFileAsync("test.txt", "new.txt"); - Assert.Contains(await storage.GetFileListAsync(), f => f.Path == "new.txt"); - await storage.DeleteFileAsync("new.txt"); - Assert.Empty(await storage.GetFileListAsync()); - } + using (storage) + { + await storage.SaveFileAsync("test.txt", "test"); + var file = (await storage.GetFileListAsync()).Single(); + Assert.NotNull(file); + Assert.Equal("test.txt", file.Path); + string content = await storage.GetFileContentsAsync("test.txt"); + Assert.Equal("test", content); + await storage.RenameFileAsync("test.txt", "new.txt"); + Assert.Contains(await storage.GetFileListAsync(), f => f.Path == "new.txt"); + await storage.DeleteFileAsync("new.txt"); + Assert.Empty(await storage.GetFileListAsync()); } + } - public virtual async Task CanRenameFilesAsync() - { - var storage = GetStorage(); - if (storage == null) - return; + public virtual async Task CanRenameFilesAsync() + { + var storage = GetStorage(); + if (storage == null) + return; - await ResetAsync(storage); + await ResetAsync(storage); - using (storage) - { - Assert.True(await storage.SaveFileAsync("test.txt", "test")); - Assert.True(await storage.RenameFileAsync("test.txt", @"archive\new.txt")); - Assert.Equal("test", await storage.GetFileContentsAsync(@"archive\new.txt")); - Assert.Single(await storage.GetFileListAsync()); - - Assert.True(await storage.SaveFileAsync("test2.txt", "test2")); - Assert.True(await storage.RenameFileAsync("test2.txt", @"archive\new.txt")); - Assert.Equal("test2", await storage.GetFileContentsAsync(@"archive\new.txt")); - Assert.Single(await storage.GetFileListAsync()); - } + using (storage) + { + Assert.True(await storage.SaveFileAsync("test.txt", "test")); + Assert.True(await storage.RenameFileAsync("test.txt", @"archive\new.txt")); + Assert.Equal("test", await storage.GetFileContentsAsync(@"archive\new.txt")); + Assert.Single(await storage.GetFileListAsync()); + + Assert.True(await storage.SaveFileAsync("test2.txt", "test2")); + Assert.True(await storage.RenameFileAsync("test2.txt", @"archive\new.txt")); + Assert.Equal("test2", await storage.GetFileContentsAsync(@"archive\new.txt")); + Assert.Single(await storage.GetFileListAsync()); } + } - protected virtual string GetTestFilePath() + protected virtual string GetTestFilePath() + { + var currentDirectory = new DirectoryInfo(PathHelper.ExpandPath(@"|DataDirectory|\")); + var currentFilePath = Path.Combine(currentDirectory.FullName, "README.md"); + while (!File.Exists(currentFilePath) && currentDirectory.Parent != null) { - var currentDirectory = new DirectoryInfo(PathHelper.ExpandPath(@"|DataDirectory|\")); - var currentFilePath = Path.Combine(currentDirectory.FullName, "README.md"); - while (!File.Exists(currentFilePath) && currentDirectory.Parent != null) - { - currentDirectory = currentDirectory.Parent; - currentFilePath = Path.Combine(currentDirectory.FullName, "README.md"); - } + currentDirectory = currentDirectory.Parent; + currentFilePath = Path.Combine(currentDirectory.FullName, "README.md"); + } - if (File.Exists(currentFilePath)) - return currentFilePath; + if (File.Exists(currentFilePath)) + return currentFilePath; - throw new ApplicationException("Unable to find test README.md file in path hierarchy."); - } + throw new ApplicationException("Unable to find test README.md file in path hierarchy."); + } - public virtual async Task CanSaveFilesAsync() - { - var storage = GetStorage(); - if (storage == null) - return; + public virtual async Task CanSaveFilesAsync() + { + var storage = GetStorage(); + if (storage == null) + return; - await ResetAsync(storage); + await ResetAsync(storage); - string readmeFile = GetTestFilePath(); - using (storage) - { - Assert.False(await storage.ExistsAsync("Foundatio.Tests.csproj")); + string readmeFile = GetTestFilePath(); + using (storage) + { + Assert.False(await storage.ExistsAsync("Foundatio.Tests.csproj")); - await using (var stream = new NonSeekableStream(File.Open(readmeFile, FileMode.Open, FileAccess.Read))) - { - bool result = await storage.SaveFileAsync("Foundatio.Tests.csproj", stream); - Assert.True(result); - } + await using (var stream = new NonSeekableStream(File.Open(readmeFile, FileMode.Open, FileAccess.Read))) + { + bool result = await storage.SaveFileAsync("Foundatio.Tests.csproj", stream); + Assert.True(result); + } - Assert.Single(await storage.GetFileListAsync()); - Assert.True(await storage.ExistsAsync("Foundatio.Tests.csproj")); + Assert.Single(await storage.GetFileListAsync()); + Assert.True(await storage.ExistsAsync("Foundatio.Tests.csproj")); - await using (var stream = await storage.GetFileStreamAsync("Foundatio.Tests.csproj")) - { - string result = await new StreamReader(stream).ReadToEndAsync(); - Assert.Equal(File.ReadAllText(readmeFile), result); - } + await using (var stream = await storage.GetFileStreamAsync("Foundatio.Tests.csproj")) + { + string result = await new StreamReader(stream).ReadToEndAsync(); + Assert.Equal(File.ReadAllText(readmeFile), result); } } + } - public virtual async Task CanDeleteEntireFolderAsync() - { - var storage = GetStorage(); - if (storage == null) - return; + public virtual async Task CanDeleteEntireFolderAsync() + { + var storage = GetStorage(); + if (storage == null) + return; - await ResetAsync(storage); + await ResetAsync(storage); - using (storage) - { - await storage.SaveFileAsync(@"x\hello.txt", "hello"); - await storage.SaveFileAsync(@"x\nested\world.csv", "nested world"); - Assert.Equal(2, (await storage.GetFileListAsync()).Count); + using (storage) + { + await storage.SaveFileAsync(@"x\hello.txt", "hello"); + await storage.SaveFileAsync(@"x\nested\world.csv", "nested world"); + Assert.Equal(2, (await storage.GetFileListAsync()).Count); - await storage.DeleteFilesAsync(@"x\*"); - Assert.Empty(await storage.GetFileListAsync()); - } + await storage.DeleteFilesAsync(@"x\*"); + Assert.Empty(await storage.GetFileListAsync()); } + } - public virtual async Task CanDeleteEntireFolderWithWildcardAsync() - { - var storage = GetStorage(); - if (storage == null) - return; + public virtual async Task CanDeleteEntireFolderWithWildcardAsync() + { + var storage = GetStorage(); + if (storage == null) + return; - await ResetAsync(storage); + await ResetAsync(storage); - using (storage) - { - await storage.SaveFileAsync(@"x\hello.txt", "hello"); - await storage.SaveFileAsync(@"x\nested\world.csv", "nested world"); - Assert.Equal(2, (await storage.GetFileListAsync()).Count); - Assert.Single(await storage.GetFileListAsync(limit: 1)); - Assert.Equal(2, (await storage.GetFileListAsync(@"x\*")).Count); - Assert.Single(await storage.GetFileListAsync(@"x\nested\*")); + using (storage) + { + await storage.SaveFileAsync(@"x\hello.txt", "hello"); + await storage.SaveFileAsync(@"x\nested\world.csv", "nested world"); + Assert.Equal(2, (await storage.GetFileListAsync()).Count); + Assert.Single(await storage.GetFileListAsync(limit: 1)); + Assert.Equal(2, (await storage.GetFileListAsync(@"x\*")).Count); + Assert.Single(await storage.GetFileListAsync(@"x\nested\*")); - await storage.DeleteFilesAsync(@"x\*"); + await storage.DeleteFilesAsync(@"x\*"); - Assert.Empty(await storage.GetFileListAsync()); - } + Assert.Empty(await storage.GetFileListAsync()); } + } - public virtual async Task CanDeleteFolderWithMultiFolderWildcardsAsync() - { - var storage = GetStorage(); - if (storage == null) - return; + public virtual async Task CanDeleteFolderWithMultiFolderWildcardsAsync() + { + var storage = GetStorage(); + if (storage == null) + return; - await ResetAsync(storage); + await ResetAsync(storage); - using (storage) + using (storage) + { + const int filesPerMonth = 5; + for (int year = 2020; year <= 2021; year++) { - const int filesPerMonth = 5; - for (int year = 2020; year <= 2021; year++) + for (int month = 1; month <= 12; month++) { - for (int month = 1; month <= 12; month++) - { - for (int index = 0; index < filesPerMonth; index++) - await storage.SaveFileAsync($"archive\\year-{year}\\month-{month:00}\\file-{index:00}.txt", "hello"); - } + for (int index = 0; index < filesPerMonth; index++) + await storage.SaveFileAsync($"archive\\year-{year}\\month-{month:00}\\file-{index:00}.txt", "hello"); } + } - _logger.LogInformation(@"List by pattern: archive\*"); - Assert.Equal(2 * 12 * filesPerMonth, (await storage.GetFileListAsync(@"archive\*")).Count); - _logger.LogInformation(@"List by pattern: archive\*month-01*"); - Assert.Equal(2 * filesPerMonth, (await storage.GetFileListAsync(@"archive\*month-01*")).Count); - _logger.LogInformation(@"List by pattern: archive\year-2020\*month-01*"); - Assert.Equal(filesPerMonth, (await storage.GetFileListAsync(@"archive\year-2020\*month-01*")).Count); + _logger.LogInformation(@"List by pattern: archive\*"); + Assert.Equal(2 * 12 * filesPerMonth, (await storage.GetFileListAsync(@"archive\*")).Count); + _logger.LogInformation(@"List by pattern: archive\*month-01*"); + Assert.Equal(2 * filesPerMonth, (await storage.GetFileListAsync(@"archive\*month-01*")).Count); + _logger.LogInformation(@"List by pattern: archive\year-2020\*month-01*"); + Assert.Equal(filesPerMonth, (await storage.GetFileListAsync(@"archive\year-2020\*month-01*")).Count); - _logger.LogInformation(@"Delete by pattern: archive\*month-01*"); - await storage.DeleteFilesAsync(@"archive\*month-01*"); + _logger.LogInformation(@"Delete by pattern: archive\*month-01*"); + await storage.DeleteFilesAsync(@"archive\*month-01*"); - Assert.Equal(2 * 11 * filesPerMonth, (await storage.GetFileListAsync()).Count); - } + Assert.Equal(2 * 11 * filesPerMonth, (await storage.GetFileListAsync()).Count); } + } - public virtual async Task CanDeleteSpecificFilesAsync() - { - var storage = GetStorage(); - if (storage == null) - return; - - await ResetAsync(storage); + public virtual async Task CanDeleteSpecificFilesAsync() + { + var storage = GetStorage(); + if (storage == null) + return; - using (storage) - { - await storage.SaveFileAsync(@"x\hello.txt", "hello"); - await storage.SaveFileAsync(@"x\nested\world.csv", "nested world"); - await storage.SaveFileAsync(@"x\nested\hello.txt", "nested hello"); - Assert.Equal(3, (await storage.GetFileListAsync()).Count); - Assert.Single(await storage.GetFileListAsync(limit: 1)); - Assert.Equal(3, (await storage.GetFileListAsync(@"x\*")).Count); - Assert.Equal(2, (await storage.GetFileListAsync(@"x\nested\*")).Count); - Assert.Equal(2, (await storage.GetFileListAsync(@"x\*.txt")).Count); - - await storage.DeleteFilesAsync(@"x\*.txt"); - - Assert.Single(await storage.GetFileListAsync()); - Assert.False(await storage.ExistsAsync(@"x\hello.txt")); - Assert.False(await storage.ExistsAsync(@"x\nested\hello.txt")); - Assert.True(await storage.ExistsAsync(@"x\nested\world.csv")); - } - } + await ResetAsync(storage); - public virtual async Task CanDeleteNestedFolderAsync() + using (storage) { - var storage = GetStorage(); - if (storage == null) - return; + await storage.SaveFileAsync(@"x\hello.txt", "hello"); + await storage.SaveFileAsync(@"x\nested\world.csv", "nested world"); + await storage.SaveFileAsync(@"x\nested\hello.txt", "nested hello"); + Assert.Equal(3, (await storage.GetFileListAsync()).Count); + Assert.Single(await storage.GetFileListAsync(limit: 1)); + Assert.Equal(3, (await storage.GetFileListAsync(@"x\*")).Count); + Assert.Equal(2, (await storage.GetFileListAsync(@"x\nested\*")).Count); + Assert.Equal(2, (await storage.GetFileListAsync(@"x\*.txt")).Count); + + await storage.DeleteFilesAsync(@"x\*.txt"); + + Assert.Single(await storage.GetFileListAsync()); + Assert.False(await storage.ExistsAsync(@"x\hello.txt")); + Assert.False(await storage.ExistsAsync(@"x\nested\hello.txt")); + Assert.True(await storage.ExistsAsync(@"x\nested\world.csv")); + } + } - await ResetAsync(storage); + public virtual async Task CanDeleteNestedFolderAsync() + { + var storage = GetStorage(); + if (storage == null) + return; - using (storage) - { - await storage.SaveFileAsync(@"x\hello.txt", "hello"); - await storage.SaveFileAsync(@"x\nested\world.csv", "nested world"); - await storage.SaveFileAsync(@"x\nested\hello.txt", "nested hello"); - Assert.Equal(3, (await storage.GetFileListAsync()).Count); - Assert.Single(await storage.GetFileListAsync(limit: 1)); - Assert.Equal(3, (await storage.GetFileListAsync(@"x\*")).Count); - Assert.Equal(2, (await storage.GetFileListAsync(@"x\nested\*")).Count); - Assert.Equal(2, (await storage.GetFileListAsync(@"x\*.txt")).Count); - - await storage.DeleteFilesAsync(@"x\nested\*"); - - Assert.Single(await storage.GetFileListAsync()); - Assert.True(await storage.ExistsAsync(@"x\hello.txt")); - Assert.False(await storage.ExistsAsync(@"x\nested\hello.txt")); - Assert.False(await storage.ExistsAsync(@"x\nested\world.csv")); - } - } + await ResetAsync(storage); - public virtual async Task CanDeleteSpecificFilesInNestedFolderAsync() + using (storage) { - var storage = GetStorage(); - if (storage == null) - return; + await storage.SaveFileAsync(@"x\hello.txt", "hello"); + await storage.SaveFileAsync(@"x\nested\world.csv", "nested world"); + await storage.SaveFileAsync(@"x\nested\hello.txt", "nested hello"); + Assert.Equal(3, (await storage.GetFileListAsync()).Count); + Assert.Single(await storage.GetFileListAsync(limit: 1)); + Assert.Equal(3, (await storage.GetFileListAsync(@"x\*")).Count); + Assert.Equal(2, (await storage.GetFileListAsync(@"x\nested\*")).Count); + Assert.Equal(2, (await storage.GetFileListAsync(@"x\*.txt")).Count); + + await storage.DeleteFilesAsync(@"x\nested\*"); + + Assert.Single(await storage.GetFileListAsync()); + Assert.True(await storage.ExistsAsync(@"x\hello.txt")); + Assert.False(await storage.ExistsAsync(@"x\nested\hello.txt")); + Assert.False(await storage.ExistsAsync(@"x\nested\world.csv")); + } + } - await ResetAsync(storage); + public virtual async Task CanDeleteSpecificFilesInNestedFolderAsync() + { + var storage = GetStorage(); + if (storage == null) + return; - using (storage) - { - await storage.SaveFileAsync(@"x\hello.txt", "hello"); - await storage.SaveFileAsync(@"x\world.csv", "world"); - await storage.SaveFileAsync(@"x\nested\world.csv", "nested world"); - await storage.SaveFileAsync(@"x\nested\hello.txt", "nested hello"); - await storage.SaveFileAsync(@"x\nested\again.txt", "nested again"); - Assert.Equal(5, (await storage.GetFileListAsync()).Count); - Assert.Single(await storage.GetFileListAsync(limit: 1)); - Assert.Equal(5, (await storage.GetFileListAsync(@"x\*")).Count); - Assert.Equal(3, (await storage.GetFileListAsync(@"x\nested\*")).Count); - Assert.Equal(3, (await storage.GetFileListAsync(@"x\*.txt")).Count); - - await storage.DeleteFilesAsync(@"x\nested\*.txt"); - - Assert.Equal(3, (await storage.GetFileListAsync()).Count); - Assert.True(await storage.ExistsAsync(@"x\hello.txt")); - Assert.True(await storage.ExistsAsync(@"x\world.csv")); - Assert.False(await storage.ExistsAsync(@"x\nested\hello.txt")); - Assert.False(await storage.ExistsAsync(@"x\nested\again.txt")); - Assert.True(await storage.ExistsAsync(@"x\nested\world.csv")); - } - } + await ResetAsync(storage); - public virtual async Task CanRoundTripSeekableStreamAsync() + using (storage) { - var storage = GetStorage(); - if (storage == null) - return; + await storage.SaveFileAsync(@"x\hello.txt", "hello"); + await storage.SaveFileAsync(@"x\world.csv", "world"); + await storage.SaveFileAsync(@"x\nested\world.csv", "nested world"); + await storage.SaveFileAsync(@"x\nested\hello.txt", "nested hello"); + await storage.SaveFileAsync(@"x\nested\again.txt", "nested again"); + Assert.Equal(5, (await storage.GetFileListAsync()).Count); + Assert.Single(await storage.GetFileListAsync(limit: 1)); + Assert.Equal(5, (await storage.GetFileListAsync(@"x\*")).Count); + Assert.Equal(3, (await storage.GetFileListAsync(@"x\nested\*")).Count); + Assert.Equal(3, (await storage.GetFileListAsync(@"x\*.txt")).Count); + + await storage.DeleteFilesAsync(@"x\nested\*.txt"); + + Assert.Equal(3, (await storage.GetFileListAsync()).Count); + Assert.True(await storage.ExistsAsync(@"x\hello.txt")); + Assert.True(await storage.ExistsAsync(@"x\world.csv")); + Assert.False(await storage.ExistsAsync(@"x\nested\hello.txt")); + Assert.False(await storage.ExistsAsync(@"x\nested\again.txt")); + Assert.True(await storage.ExistsAsync(@"x\nested\world.csv")); + } + } - await ResetAsync(storage); + public virtual async Task CanRoundTripSeekableStreamAsync() + { + var storage = GetStorage(); + if (storage == null) + return; - using (storage) - { - const string path = "user.xml"; - var element = XElement.Parse("Blake"); + await ResetAsync(storage); - using (var memoryStream = new MemoryStream()) - { - _logger.LogTrace("Saving xml to stream with position {Position}.", memoryStream.Position); - element.Save(memoryStream, SaveOptions.DisableFormatting); + using (storage) + { + const string path = "user.xml"; + var element = XElement.Parse("Blake"); - memoryStream.Seek(0, SeekOrigin.Begin); - _logger.LogTrace("Saving contents with position {Position}", memoryStream.Position); - await storage.SaveFileAsync(path, memoryStream); - _logger.LogTrace("Saved contents with position {Position}.", memoryStream.Position); - } + using (var memoryStream = new MemoryStream()) + { + _logger.LogTrace("Saving xml to stream with position {Position}.", memoryStream.Position); + element.Save(memoryStream, SaveOptions.DisableFormatting); - await using var stream = await storage.GetFileStreamAsync(path); - var actual = XElement.Load(stream); - Assert.Equal(element.ToString(SaveOptions.DisableFormatting), actual.ToString(SaveOptions.DisableFormatting)); + memoryStream.Seek(0, SeekOrigin.Begin); + _logger.LogTrace("Saving contents with position {Position}", memoryStream.Position); + await storage.SaveFileAsync(path, memoryStream); + _logger.LogTrace("Saved contents with position {Position}.", memoryStream.Position); } + + await using var stream = await storage.GetFileStreamAsync(path); + var actual = XElement.Load(stream); + Assert.Equal(element.ToString(SaveOptions.DisableFormatting), actual.ToString(SaveOptions.DisableFormatting)); } + } - public virtual async Task WillRespectStreamOffsetAsync() - { - var storage = GetStorage(); - if (storage == null) - return; + public virtual async Task WillRespectStreamOffsetAsync() + { + var storage = GetStorage(); + if (storage == null) + return; - await ResetAsync(storage); + await ResetAsync(storage); - using (storage) + using (storage) + { + string path = "blake.txt"; + using (var memoryStream = new MemoryStream()) { - string path = "blake.txt"; - using (var memoryStream = new MemoryStream()) + long offset; + await using (var writer = new StreamWriter(memoryStream, Encoding.UTF8, 1024, true)) { - long offset; - await using (var writer = new StreamWriter(memoryStream, Encoding.UTF8, 1024, true)) - { - writer.AutoFlush = true; - await writer.WriteAsync("Eric"); - offset = memoryStream.Position; - await writer.WriteAsync("Blake"); - await writer.FlushAsync(); - } - - memoryStream.Seek(offset, SeekOrigin.Begin); - await storage.SaveFileAsync(path, memoryStream); + writer.AutoFlush = true; + await writer.WriteAsync("Eric"); + offset = memoryStream.Position; + await writer.WriteAsync("Blake"); + await writer.FlushAsync(); } - Assert.Equal("Blake", await storage.GetFileContentsAsync(path)); + memoryStream.Seek(offset, SeekOrigin.Begin); + await storage.SaveFileAsync(path, memoryStream); } + + Assert.Equal("Blake", await storage.GetFileContentsAsync(path)); } + } - public virtual async Task WillWriteStreamContentAsync() - { + public virtual async Task WillWriteStreamContentAsync() + { - const string testContent = "test"; - const string path = "created.txt"; + const string testContent = "test"; + const string path = "created.txt"; - var storage = GetStorage(); - if (storage == null) - return; + var storage = GetStorage(); + if (storage == null) + return; - await ResetAsync(storage); + await ResetAsync(storage); - using (storage) + using (storage) + { + await using (var writer = new StreamWriter(await storage.GetFileStreamAsync(path, StreamMode.Write), Encoding.UTF8, 1024, false)) { - await using (var writer = new StreamWriter(await storage.GetFileStreamAsync(path, StreamMode.Write), Encoding.UTF8, 1024, false)) - { - await writer.WriteAsync(testContent); - } + await writer.WriteAsync(testContent); + } - string content = await storage.GetFileContentsAsync(path); + string content = await storage.GetFileContentsAsync(path); - Assert.Equal(testContent, content); - } + Assert.Equal(testContent, content); } + } - protected virtual async Task ResetAsync(IFileStorage storage) - { - if (storage == null) - return; + protected virtual async Task ResetAsync(IFileStorage storage) + { + if (storage == null) + return; - _logger.LogInformation("Deleting all files..."); - await storage.DeleteFilesAsync(); - _logger.LogInformation("Asserting empty files..."); - Assert.Empty(await storage.GetFileListAsync(limit: 10000)); - } + _logger.LogInformation("Deleting all files..."); + await storage.DeleteFilesAsync(); + _logger.LogInformation("Asserting empty files..."); + Assert.Empty(await storage.GetFileListAsync(limit: 10000)); + } - public virtual async Task CanConcurrentlyManageFilesAsync() - { - var storage = GetStorage(); - if (storage == null) - return; + public virtual async Task CanConcurrentlyManageFilesAsync() + { + var storage = GetStorage(); + if (storage == null) + return; - await ResetAsync(storage); + await ResetAsync(storage); - using (storage) - { - const string queueFolder = "q"; - var queueItems = new BlockingCollection(); + using (storage) + { + const string queueFolder = "q"; + var queueItems = new BlockingCollection(); - var info = await storage.GetFileInfoAsync("nope"); - Assert.Null(info); + var info = await storage.GetFileInfoAsync("nope"); + Assert.Null(info); - await Run.InParallelAsync(10, async i => - { - var ev = new PostInfo - { - ApiVersion = 2, - CharSet = "utf8", - ContentEncoding = "application/json", - Data = Encoding.UTF8.GetBytes("{}"), - IpAddress = "127.0.0.1", - MediaType = "gzip", - ProjectId = i.ToString(), - UserAgent = "test" - }; - - await storage.SaveObjectAsync(Path.Combine(queueFolder, i + ".json"), ev); - queueItems.Add(i); - }); - - Assert.Equal(10, (await storage.GetFileListAsync()).Count); - - await Run.InParallelAsync(10, async i => + await Run.InParallelAsync(10, async i => + { + var ev = new PostInfo { - string path = Path.Combine(queueFolder, queueItems.Random() + ".json"); - var eventPost = await storage.GetEventPostAndSetActiveAsync(Path.Combine(queueFolder, RandomData.GetInt(0, 25) + ".json"), _logger); - if (eventPost == null) - return; - - if (RandomData.GetBool()) - { - await storage.CompleteEventPostAsync(path, eventPost.ProjectId, SystemClock.UtcNow, true, _logger); - } - else - await storage.SetNotActiveAsync(path, _logger); - }); - } - } + ApiVersion = 2, + CharSet = "utf8", + ContentEncoding = "application/json", + Data = Encoding.UTF8.GetBytes("{}"), + IpAddress = "127.0.0.1", + MediaType = "gzip", + ProjectId = i.ToString(), + UserAgent = "test" + }; + + await storage.SaveObjectAsync(Path.Combine(queueFolder, i + ".json"), ev); + queueItems.Add(i); + }); - public virtual void CanUseDataDirectory() - { - const string DATA_DIRECTORY_QUEUE_FOLDER = @"|DataDirectory|\Queue"; + Assert.Equal(10, (await storage.GetFileListAsync()).Count); - var storage = new FolderFileStorage(new FolderFileStorageOptions + await Run.InParallelAsync(10, async i => { - Folder = DATA_DIRECTORY_QUEUE_FOLDER + string path = Path.Combine(queueFolder, queueItems.Random() + ".json"); + var eventPost = await storage.GetEventPostAndSetActiveAsync(Path.Combine(queueFolder, RandomData.GetInt(0, 25) + ".json"), _logger); + if (eventPost == null) + return; + + if (RandomData.GetBool()) + { + await storage.CompleteEventPostAsync(path, eventPost.ProjectId, SystemClock.UtcNow, true, _logger); + } + else + await storage.SetNotActiveAsync(path, _logger); }); - Assert.NotNull(storage.Folder); - Assert.NotEqual(DATA_DIRECTORY_QUEUE_FOLDER, storage.Folder); - Assert.True(storage.Folder.EndsWith("Queue" + Path.DirectorySeparatorChar), storage.Folder); } } - public class PostInfo + public virtual void CanUseDataDirectory() { - public int ApiVersion { get; set; } - public string CharSet { get; set; } - public string ContentEncoding { get; set; } - public byte[] Data { get; set; } - public string IpAddress { get; set; } - public string MediaType { get; set; } - public string ProjectId { get; set; } - public string UserAgent { get; set; } + const string DATA_DIRECTORY_QUEUE_FOLDER = @"|DataDirectory|\Queue"; + + var storage = new FolderFileStorage(new FolderFileStorageOptions + { + Folder = DATA_DIRECTORY_QUEUE_FOLDER + }); + Assert.NotNull(storage.Folder); + Assert.NotEqual(DATA_DIRECTORY_QUEUE_FOLDER, storage.Folder); + Assert.True(storage.Folder.EndsWith("Queue" + Path.DirectorySeparatorChar), storage.Folder); } +} + +public class PostInfo +{ + public int ApiVersion { get; set; } + public string CharSet { get; set; } + public string ContentEncoding { get; set; } + public byte[] Data { get; set; } + public string IpAddress { get; set; } + public string MediaType { get; set; } + public string ProjectId { get; set; } + public string UserAgent { get; set; } +} - public static class StorageExtensions +public static class StorageExtensions +{ + public static async Task GetEventPostAndSetActiveAsync(this IFileStorage storage, string path, ILogger logger = null) { - public static async Task GetEventPostAndSetActiveAsync(this IFileStorage storage, string path, ILogger logger = null) + PostInfo eventPostInfo = null; + try { - PostInfo eventPostInfo = null; - try - { - eventPostInfo = await storage.GetObjectAsync(path); - if (eventPostInfo == null) - return null; - - if (!await storage.ExistsAsync(path + ".x") && !await storage.SaveFileAsync(path + ".x", String.Empty)) - return null; - } - catch (Exception ex) - { - if (logger != null && logger.IsEnabled(LogLevel.Error)) - logger.LogError(ex, "Error retrieving event post data {Path}: {Message}", path, ex.Message); + eventPostInfo = await storage.GetObjectAsync(path); + if (eventPostInfo == null) return null; - } - return eventPostInfo; + if (!await storage.ExistsAsync(path + ".x") && !await storage.SaveFileAsync(path + ".x", String.Empty)) + return null; } - - public static async Task SetNotActiveAsync(this IFileStorage storage, string path, ILogger logger = null) + catch (Exception ex) { - try - { - return await storage.DeleteFileAsync(path + ".x"); - } - catch (Exception ex) - { - if (logger != null && logger.IsEnabled(LogLevel.Error)) - logger.LogError(ex, "Error deleting work marker {Path}: {Message}", path, ex.Message); - } - - return false; + if (logger != null && logger.IsEnabled(LogLevel.Error)) + logger.LogError(ex, "Error retrieving event post data {Path}: {Message}", path, ex.Message); + return null; } - public static async Task CompleteEventPostAsync(this IFileStorage storage, string path, string projectId, DateTime created, bool shouldArchive = true, ILogger logger = null) + return eventPostInfo; + } + + public static async Task SetNotActiveAsync(this IFileStorage storage, string path, ILogger logger = null) + { + try + { + return await storage.DeleteFileAsync(path + ".x"); + } + catch (Exception ex) { - // don't move files that are already in the archive - if (path.StartsWith("archive")) - return true; + if (logger != null && logger.IsEnabled(LogLevel.Error)) + logger.LogError(ex, "Error deleting work marker {Path}: {Message}", path, ex.Message); + } + + return false; + } + + public static async Task CompleteEventPostAsync(this IFileStorage storage, string path, string projectId, DateTime created, bool shouldArchive = true, ILogger logger = null) + { + // don't move files that are already in the archive + if (path.StartsWith("archive")) + return true; - string archivePath = $"archive\\{projectId}\\{created.ToString("yy\\\\MM\\\\dd")}\\{Path.GetFileName(path)}"; + string archivePath = $"archive\\{projectId}\\{created.ToString("yy\\\\MM\\\\dd")}\\{Path.GetFileName(path)}"; - try + try + { + if (shouldArchive) { - if (shouldArchive) - { - if (!await storage.RenameFileAsync(path, archivePath)) - return false; - } - else - { - if (!await storage.DeleteFileAsync(path)) - return false; - } + if (!await storage.RenameFileAsync(path, archivePath)) + return false; } - catch (Exception ex) + else { - if (logger != null && logger.IsEnabled(LogLevel.Error)) - logger?.LogError(ex, "Error archiving event post data {Path}: {Message}", path, ex.Message); - return false; + if (!await storage.DeleteFileAsync(path)) + return false; } + } + catch (Exception ex) + { + if (logger != null && logger.IsEnabled(LogLevel.Error)) + logger?.LogError(ex, "Error archiving event post data {Path}: {Message}", path, ex.Message); + return false; + } - await storage.SetNotActiveAsync(path); + await storage.SetNotActiveAsync(path); - return true; - } + return true; } } diff --git a/src/Foundatio.TestHarness/Utility/BenchmarkToJson.cs b/src/Foundatio.TestHarness/Utility/BenchmarkToJson.cs index f7b0b05ca..2ce0b5ebf 100644 --- a/src/Foundatio.TestHarness/Utility/BenchmarkToJson.cs +++ b/src/Foundatio.TestHarness/Utility/BenchmarkToJson.cs @@ -4,46 +4,45 @@ using BenchmarkDotNet.Loggers; using BenchmarkDotNet.Reports; -namespace Foundatio.TestHarness.Utility -{ - public class StringBenchmarkLogger : ILogger - { - private readonly StringBuilder _buffer = new(); +namespace Foundatio.TestHarness.Utility; - public string Id => Guid.NewGuid().ToString(); - public int Priority => 1; +public class StringBenchmarkLogger : ILogger +{ + private readonly StringBuilder _buffer = new(); - public void Write(LogKind logKind, string text) - { - _buffer.Append(text); - } + public string Id => Guid.NewGuid().ToString(); + public int Priority => 1; - public void WriteLine() - { - _buffer.AppendLine(); - } + public void Write(LogKind logKind, string text) + { + _buffer.Append(text); + } - public void WriteLine(LogKind logKind, string text) - { - _buffer.AppendLine(text); - } + public void WriteLine() + { + _buffer.AppendLine(); + } - public override string ToString() - { - return _buffer.ToString(); - } + public void WriteLine(LogKind logKind, string text) + { + _buffer.AppendLine(text); + } - public void Flush() { } + public override string ToString() + { + return _buffer.ToString(); } - public static class BenchmarkSummaryExtensions + public void Flush() { } +} + +public static class BenchmarkSummaryExtensions +{ + public static string ToJson(this Summary summary, bool indentJson = true) { - public static string ToJson(this Summary summary, bool indentJson = true) - { - var exporter = new JsonExporter(indentJson: indentJson); - var logger = new StringBenchmarkLogger(); - exporter.ExportToLog(summary, logger); - return logger.ToString(); - } + var exporter = new JsonExporter(indentJson: indentJson); + var logger = new StringBenchmarkLogger(); + exporter.ExportToLog(summary, logger); + return logger.ToString(); } } diff --git a/src/Foundatio.TestHarness/Utility/Configuration.cs b/src/Foundatio.TestHarness/Utility/Configuration.cs index ad528ab1b..124af1eec 100644 --- a/src/Foundatio.TestHarness/Utility/Configuration.cs +++ b/src/Foundatio.TestHarness/Utility/Configuration.cs @@ -2,43 +2,42 @@ using System.Reflection; using Microsoft.Extensions.Configuration; -namespace Foundatio.Tests.Utility +namespace Foundatio.Tests.Utility; + +public static class Configuration { - public static class Configuration + private static readonly IConfiguration _configuration; + static Configuration() { - private static readonly IConfiguration _configuration; - static Configuration() - { - _configuration = new ConfigurationBuilder() - .SetBasePath(GetBasePath()) - .AddJsonFile("appsettings.json", true, true) - .AddEnvironmentVariables() - .Build(); - } - - public static IConfigurationSection GetSection(string name) - { - return _configuration.GetSection(name); - } + _configuration = new ConfigurationBuilder() + .SetBasePath(GetBasePath()) + .AddJsonFile("appsettings.json", true, true) + .AddEnvironmentVariables() + .Build(); + } - public static string GetConnectionString(string name) - { - return _configuration.GetConnectionString(name); - } + public static IConfigurationSection GetSection(string name) + { + return _configuration.GetSection(name); + } - private static string GetBasePath() - { - string basePath = Path.GetDirectoryName(typeof(Configuration).GetTypeInfo().Assembly.Location); + public static string GetConnectionString(string name) + { + return _configuration.GetConnectionString(name); + } - for (int i = 0; i < 5; i++) - { - if (File.Exists(Path.Combine(basePath, "appsettings.json"))) - return Path.GetFullPath(basePath); + private static string GetBasePath() + { + string basePath = Path.GetDirectoryName(typeof(Configuration).GetTypeInfo().Assembly.Location); - basePath += "..\\"; - } + for (int i = 0; i < 5; i++) + { + if (File.Exists(Path.Combine(basePath, "appsettings.json"))) + return Path.GetFullPath(basePath); - return Path.GetFullPath("."); + basePath += "..\\"; } + + return Path.GetFullPath("."); } } diff --git a/src/Foundatio.TestHarness/Utility/NonSeekableStream.cs b/src/Foundatio.TestHarness/Utility/NonSeekableStream.cs index 1507f4dff..acee78c00 100644 --- a/src/Foundatio.TestHarness/Utility/NonSeekableStream.cs +++ b/src/Foundatio.TestHarness/Utility/NonSeekableStream.cs @@ -1,66 +1,65 @@ using System; using System.IO; -namespace Foundatio.Tests.Utility +namespace Foundatio.Tests.Utility; + +public class NonSeekableStream : Stream { - public class NonSeekableStream : Stream - { - private readonly Stream _stream; + private readonly Stream _stream; - public NonSeekableStream(Stream stream) - { - _stream = stream; - } + public NonSeekableStream(Stream stream) + { + _stream = stream; + } - public override bool CanRead => _stream.CanRead; + public override bool CanRead => _stream.CanRead; - public override bool CanSeek => false; + public override bool CanSeek => false; - public override bool CanWrite => _stream.CanWrite; + public override bool CanWrite => _stream.CanWrite; - public override void Flush() - { - _stream.Flush(); - } + public override void Flush() + { + _stream.Flush(); + } - public override long Length => throw new NotSupportedException(); + public override long Length => throw new NotSupportedException(); - public override long Position - { - get => _stream.Position; - set => throw new NotSupportedException(); - } + public override long Position + { + get => _stream.Position; + set => throw new NotSupportedException(); + } - public override int Read(byte[] buffer, int offset, int count) - { - return _stream.Read(buffer, offset, count); - } + public override int Read(byte[] buffer, int offset, int count) + { + return _stream.Read(buffer, offset, count); + } - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotImplementedException(); - } + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotImplementedException(); + } - public override void SetLength(long value) - { - throw new NotSupportedException(); - } + public override void SetLength(long value) + { + throw new NotSupportedException(); + } - public override void Write(byte[] buffer, int offset, int count) - { - _stream.Write(buffer, offset, count); - } + public override void Write(byte[] buffer, int offset, int count) + { + _stream.Write(buffer, offset, count); + } - public override void Close() - { - _stream.Close(); - base.Close(); - } + public override void Close() + { + _stream.Close(); + base.Close(); + } - protected override void Dispose(bool disposing) - { - _stream.Dispose(); - base.Dispose(disposing); - } + protected override void Dispose(bool disposing) + { + _stream.Dispose(); + base.Dispose(disposing); } } diff --git a/src/Foundatio.Utf8Json/Utf8JsonSerializer.cs b/src/Foundatio.Utf8Json/Utf8JsonSerializer.cs index 6db6775fb..794d0fcdb 100644 --- a/src/Foundatio.Utf8Json/Utf8JsonSerializer.cs +++ b/src/Foundatio.Utf8Json/Utf8JsonSerializer.cs @@ -3,25 +3,24 @@ using Utf8Json; using Utf8Json.Resolvers; -namespace Foundatio.Serializer +namespace Foundatio.Serializer; + +public class Utf8JsonSerializer : ITextSerializer { - public class Utf8JsonSerializer : ITextSerializer - { - private readonly IJsonFormatterResolver _formatterResolver; + private readonly IJsonFormatterResolver _formatterResolver; - public Utf8JsonSerializer(IJsonFormatterResolver resolver = null) - { - _formatterResolver = resolver ?? StandardResolver.Default; - } + public Utf8JsonSerializer(IJsonFormatterResolver resolver = null) + { + _formatterResolver = resolver ?? StandardResolver.Default; + } - public void Serialize(object data, Stream output) - { - JsonSerializer.NonGeneric.Serialize(data.GetType(), output, data, _formatterResolver); - } + public void Serialize(object data, Stream output) + { + JsonSerializer.NonGeneric.Serialize(data.GetType(), output, data, _formatterResolver); + } - public object Deserialize(Stream input, Type objectType) - { - return JsonSerializer.NonGeneric.Deserialize(objectType, input, _formatterResolver); - } + public object Deserialize(Stream input, Type objectType) + { + return JsonSerializer.NonGeneric.Deserialize(objectType, input, _formatterResolver); } } diff --git a/src/Foundatio.Xunit/Logging/LogEntry.cs b/src/Foundatio.Xunit/Logging/LogEntry.cs index 32a7ff7db..6f68b732b 100644 --- a/src/Foundatio.Xunit/Logging/LogEntry.cs +++ b/src/Foundatio.Xunit/Logging/LogEntry.cs @@ -2,38 +2,37 @@ using System.Collections.Generic; using Microsoft.Extensions.Logging; -namespace Foundatio.Xunit +namespace Foundatio.Xunit; + +public class LogEntry { - public class LogEntry - { - public DateTime Date { get; set; } - public string CategoryName { get; set; } - public LogLevel LogLevel { get; set; } - public object[] Scopes { get; set; } - public EventId EventId { get; set; } - public object State { get; set; } - public Exception Exception { get; set; } - public Func Formatter { get; set; } - public IDictionary Properties { get; set; } = new Dictionary(); + public DateTime Date { get; set; } + public string CategoryName { get; set; } + public LogLevel LogLevel { get; set; } + public object[] Scopes { get; set; } + public EventId EventId { get; set; } + public object State { get; set; } + public Exception Exception { get; set; } + public Func Formatter { get; set; } + public IDictionary Properties { get; set; } = new Dictionary(); - public string Message => Formatter(State, Exception); + public string Message => Formatter(State, Exception); - public override string ToString() - { - return String.Concat("", Date.ToString("mm:ss.fffff"), " ", LogLevel.ToString().Substring(0, 1).ToUpper(), ":", CategoryName, " - ", Message); - } + public override string ToString() + { + return String.Concat("", Date.ToString("mm:ss.fffff"), " ", LogLevel.ToString().Substring(0, 1).ToUpper(), ":", CategoryName, " - ", Message); + } - public string ToString(bool useFullCategory) + public string ToString(bool useFullCategory) + { + string category = CategoryName; + if (!useFullCategory) { - string category = CategoryName; - if (!useFullCategory) - { - int lastDot = category.LastIndexOf('.'); - if (lastDot >= 0) - category = category.Substring(lastDot + 1); - } - - return String.Concat("", Date.ToString("mm:ss.fffff"), " ", LogLevel.ToString().Substring(0, 1).ToUpper(), ":", category, " - ", Message); + int lastDot = category.LastIndexOf('.'); + if (lastDot >= 0) + category = category.Substring(lastDot + 1); } + + return String.Concat("", Date.ToString("mm:ss.fffff"), " ", LogLevel.ToString().Substring(0, 1).ToUpper(), ":", category, " - ", Message); } } diff --git a/src/Foundatio.Xunit/Logging/TestLogger.cs b/src/Foundatio.Xunit/Logging/TestLogger.cs index a9c530d50..2d525cade 100644 --- a/src/Foundatio.Xunit/Logging/TestLogger.cs +++ b/src/Foundatio.Xunit/Logging/TestLogger.cs @@ -6,108 +6,107 @@ using Foundatio.Utility; using Microsoft.Extensions.Logging; -namespace Foundatio.Xunit +namespace Foundatio.Xunit; + +internal class TestLogger : ILogger { - internal class TestLogger : ILogger + private readonly TestLoggerFactory _loggerFactory; + private readonly string _categoryName; + + public TestLogger(string categoryName, TestLoggerFactory loggerFactory) { - private readonly TestLoggerFactory _loggerFactory; - private readonly string _categoryName; + _loggerFactory = loggerFactory; + _categoryName = categoryName; + } - public TestLogger(string categoryName, TestLoggerFactory loggerFactory) - { - _loggerFactory = loggerFactory; - _categoryName = categoryName; - } + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + if (!_loggerFactory.IsEnabled(_categoryName, logLevel)) + return; - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + object[] scopes = CurrentScopeStack.Reverse().ToArray(); + var logEntry = new LogEntry + { + Date = SystemClock.UtcNow, + LogLevel = logLevel, + EventId = eventId, + State = state, + Exception = exception, + Formatter = (s, e) => formatter((TState)s, e), + CategoryName = _categoryName, + Scopes = scopes + }; + + switch (state) { - if (!_loggerFactory.IsEnabled(_categoryName, logLevel)) - return; - - object[] scopes = CurrentScopeStack.Reverse().ToArray(); - var logEntry = new LogEntry - { - Date = SystemClock.UtcNow, - LogLevel = logLevel, - EventId = eventId, - State = state, - Exception = exception, - Formatter = (s, e) => formatter((TState)s, e), - CategoryName = _categoryName, - Scopes = scopes - }; - - switch (state) - { - //case LogData logData: - // logEntry.Properties["CallerMemberName"] = logData.MemberName; - // logEntry.Properties["CallerFilePath"] = logData.FilePath; - // logEntry.Properties["CallerLineNumber"] = logData.LineNumber; - - // foreach (var property in logData.Properties) - // logEntry.Properties[property.Key] = property.Value; - // break; - case IDictionary logDictionary: - foreach (var property in logDictionary) - logEntry.Properties[property.Key] = property.Value; - break; - } - - foreach (object scope in scopes) - { - if (!(scope is IDictionary scopeData)) - continue; - - foreach (var property in scopeData) + //case LogData logData: + // logEntry.Properties["CallerMemberName"] = logData.MemberName; + // logEntry.Properties["CallerFilePath"] = logData.FilePath; + // logEntry.Properties["CallerLineNumber"] = logData.LineNumber; + + // foreach (var property in logData.Properties) + // logEntry.Properties[property.Key] = property.Value; + // break; + case IDictionary logDictionary: + foreach (var property in logDictionary) logEntry.Properties[property.Key] = property.Value; - } - - _loggerFactory.AddLogEntry(logEntry); + break; } - public bool IsEnabled(LogLevel logLevel) + foreach (object scope in scopes) { - return _loggerFactory.IsEnabled(_categoryName, logLevel); + if (!(scope is IDictionary scopeData)) + continue; + + foreach (var property in scopeData) + logEntry.Properties[property.Key] = property.Value; } - public IDisposable BeginScope(TState state) - { - if (state == null) - throw new ArgumentNullException(nameof(state)); + _loggerFactory.AddLogEntry(logEntry); + } - return Push(state); - } + public bool IsEnabled(LogLevel logLevel) + { + return _loggerFactory.IsEnabled(_categoryName, logLevel); + } - public IDisposable BeginScope(Func scopeFactory, TState state) - { - if (state == null) - throw new ArgumentNullException(nameof(state)); + public IDisposable BeginScope(TState state) + { + if (state == null) + throw new ArgumentNullException(nameof(state)); - return Push(scopeFactory(state)); - } + return Push(state); + } - private static readonly AsyncLocal _currentScopeStack = new(); + public IDisposable BeginScope(Func scopeFactory, TState state) + { + if (state == null) + throw new ArgumentNullException(nameof(state)); - private sealed class Wrapper - { - public ImmutableStack Value { get; set; } - } + return Push(scopeFactory(state)); + } - private static ImmutableStack CurrentScopeStack - { - get => _currentScopeStack.Value?.Value ?? ImmutableStack.Create(); - set => _currentScopeStack.Value = new Wrapper { Value = value }; - } + private static readonly AsyncLocal _currentScopeStack = new(); - private static IDisposable Push(object state) - { - CurrentScopeStack = CurrentScopeStack.Push(state); - return new DisposableAction(Pop); - } + private sealed class Wrapper + { + public ImmutableStack Value { get; set; } + } - private static void Pop() - { - CurrentScopeStack = CurrentScopeStack.Pop(); - } + private static ImmutableStack CurrentScopeStack + { + get => _currentScopeStack.Value?.Value ?? ImmutableStack.Create(); + set => _currentScopeStack.Value = new Wrapper { Value = value }; + } + + private static IDisposable Push(object state) + { + CurrentScopeStack = CurrentScopeStack.Push(state); + return new DisposableAction(Pop); + } + + private static void Pop() + { + CurrentScopeStack = CurrentScopeStack.Pop(); } } diff --git a/src/Foundatio.Xunit/Logging/TestLoggerFactory.cs b/src/Foundatio.Xunit/Logging/TestLoggerFactory.cs index 57c8b16dc..7379a3f6e 100644 --- a/src/Foundatio.Xunit/Logging/TestLoggerFactory.cs +++ b/src/Foundatio.Xunit/Logging/TestLoggerFactory.cs @@ -5,79 +5,78 @@ using Microsoft.Extensions.Logging; using Xunit.Abstractions; -namespace Foundatio.Xunit +namespace Foundatio.Xunit; + +public class TestLoggerFactory : ILoggerFactory { - public class TestLoggerFactory : ILoggerFactory - { - private readonly Dictionary _logLevels = new(); - private readonly Queue _logEntries = new(); - private readonly Action _writeLogEntryFunc; + private readonly Dictionary _logLevels = new(); + private readonly Queue _logEntries = new(); + private readonly Action _writeLogEntryFunc; - public TestLoggerFactory() - { - _writeLogEntryFunc = e => { }; - } + public TestLoggerFactory() + { + _writeLogEntryFunc = e => { }; + } - public TestLoggerFactory(Action writeLogEntryFunc) - { - _writeLogEntryFunc = writeLogEntryFunc; - } + public TestLoggerFactory(Action writeLogEntryFunc) + { + _writeLogEntryFunc = writeLogEntryFunc; + } - public TestLoggerFactory(ITestOutputHelper output) : this(e => output.WriteLine(e.ToString(false))) { } + public TestLoggerFactory(ITestOutputHelper output) : this(e => output.WriteLine(e.ToString(false))) { } - public LogLevel MinimumLevel { get; set; } = LogLevel.Information; - public IReadOnlyList LogEntries => _logEntries.ToArray(); - public int MaxLogEntriesToStore = 100; - public int MaxLogEntriesToWrite = 1000; + public LogLevel MinimumLevel { get; set; } = LogLevel.Information; + public IReadOnlyList LogEntries => _logEntries.ToArray(); + public int MaxLogEntriesToStore = 100; + public int MaxLogEntriesToWrite = 1000; - internal void AddLogEntry(LogEntry logEntry) + internal void AddLogEntry(LogEntry logEntry) + { + lock (_logEntries) { - lock (_logEntries) - { - _logEntries.Enqueue(logEntry); - - if (_logEntries.Count > MaxLogEntriesToStore) - _logEntries.Dequeue(); - } - - if (_writeLogEntryFunc == null || _logEntriesWritten >= MaxLogEntriesToWrite) - return; - - try - { - _writeLogEntryFunc(logEntry); - Interlocked.Increment(ref _logEntriesWritten); - } - catch (Exception) { } + _logEntries.Enqueue(logEntry); + + if (_logEntries.Count > MaxLogEntriesToStore) + _logEntries.Dequeue(); } - private int _logEntriesWritten = 0; + if (_writeLogEntryFunc == null || _logEntriesWritten >= MaxLogEntriesToWrite) + return; - public ILogger CreateLogger(string categoryName) + try { - return new TestLogger(categoryName, this); + _writeLogEntryFunc(logEntry); + Interlocked.Increment(ref _logEntriesWritten); } + catch (Exception) { } + } - public void AddProvider(ILoggerProvider loggerProvider) { } + private int _logEntriesWritten = 0; - public bool IsEnabled(string category, LogLevel logLevel) - { - if (_logLevels.TryGetValue(category, out var categoryLevel)) - return logLevel >= categoryLevel; + public ILogger CreateLogger(string categoryName) + { + return new TestLogger(categoryName, this); + } - return logLevel >= MinimumLevel; - } + public void AddProvider(ILoggerProvider loggerProvider) { } - public void SetLogLevel(string category, LogLevel minLogLevel) - { - _logLevels[category] = minLogLevel; - } + public bool IsEnabled(string category, LogLevel logLevel) + { + if (_logLevels.TryGetValue(category, out var categoryLevel)) + return logLevel >= categoryLevel; - public void SetLogLevel(LogLevel minLogLevel) - { - SetLogLevel(TypeHelper.GetTypeDisplayName(typeof(T)), minLogLevel); - } + return logLevel >= MinimumLevel; + } - public void Dispose() { } + public void SetLogLevel(string category, LogLevel minLogLevel) + { + _logLevels[category] = minLogLevel; } + + public void SetLogLevel(LogLevel minLogLevel) + { + SetLogLevel(TypeHelper.GetTypeDisplayName(typeof(T)), minLogLevel); + } + + public void Dispose() { } } diff --git a/src/Foundatio.Xunit/Logging/TestWithLoggingBase.cs b/src/Foundatio.Xunit/Logging/TestWithLoggingBase.cs index f037f30c2..a3e91ba7a 100644 --- a/src/Foundatio.Xunit/Logging/TestWithLoggingBase.cs +++ b/src/Foundatio.Xunit/Logging/TestWithLoggingBase.cs @@ -1,18 +1,17 @@ using Microsoft.Extensions.Logging; using Xunit.Abstractions; -namespace Foundatio.Xunit -{ - public abstract class TestWithLoggingBase - { - protected readonly ILogger _logger; +namespace Foundatio.Xunit; - protected TestWithLoggingBase(ITestOutputHelper output) - { - Log = new TestLoggerFactory(output); - _logger = Log.CreateLogger(GetType()); - } +public abstract class TestWithLoggingBase +{ + protected readonly ILogger _logger; - protected TestLoggerFactory Log { get; } + protected TestWithLoggingBase(ITestOutputHelper output) + { + Log = new TestLoggerFactory(output); + _logger = Log.CreateLogger(GetType()); } + + protected TestLoggerFactory Log { get; } } diff --git a/src/Foundatio.Xunit/Retry/DelayedMessageBus.cs b/src/Foundatio.Xunit/Retry/DelayedMessageBus.cs index b30092bb8..f944c8ffc 100644 --- a/src/Foundatio.Xunit/Retry/DelayedMessageBus.cs +++ b/src/Foundatio.Xunit/Retry/DelayedMessageBus.cs @@ -2,36 +2,35 @@ using Xunit.Abstractions; using Xunit.Sdk; -namespace Foundatio.Xunit +namespace Foundatio.Xunit; + +/// +/// Used to capture messages to potentially be forwarded later. Messages are forwarded by +/// disposing of the message bus. +/// +public class DelayedMessageBus : IMessageBus { - /// - /// Used to capture messages to potentially be forwarded later. Messages are forwarded by - /// disposing of the message bus. - /// - public class DelayedMessageBus : IMessageBus - { - private readonly IMessageBus innerBus; - private readonly List messages = new(); + private readonly IMessageBus innerBus; + private readonly List messages = new(); - public DelayedMessageBus(IMessageBus innerBus) - { - this.innerBus = innerBus; - } + public DelayedMessageBus(IMessageBus innerBus) + { + this.innerBus = innerBus; + } - public bool QueueMessage(IMessageSinkMessage message) - { - lock (messages) - messages.Add(message); + public bool QueueMessage(IMessageSinkMessage message) + { + lock (messages) + messages.Add(message); - // No way to ask the inner bus if they want to cancel without sending them the message, so - // we just go ahead and continue always. - return true; - } + // No way to ask the inner bus if they want to cancel without sending them the message, so + // we just go ahead and continue always. + return true; + } - public void Dispose() - { - foreach (var message in messages) - innerBus.QueueMessage(message); - } + public void Dispose() + { + foreach (var message in messages) + innerBus.QueueMessage(message); } } diff --git a/src/Foundatio.Xunit/Retry/RetryAttribute.cs b/src/Foundatio.Xunit/Retry/RetryAttribute.cs index d7f8f3997..491ad9759 100644 --- a/src/Foundatio.Xunit/Retry/RetryAttribute.cs +++ b/src/Foundatio.Xunit/Retry/RetryAttribute.cs @@ -1,23 +1,22 @@ using Xunit; using Xunit.Sdk; -namespace Foundatio.Xunit +namespace Foundatio.Xunit; + +/// +/// Works just like [Fact] except that failures are retried (by default, 3 times). +/// +[XunitTestCaseDiscoverer("Foundatio.Xunit.RetryFactDiscoverer", "Foundatio.TestHarness")] +public class RetryFactAttribute : FactAttribute { - /// - /// Works just like [Fact] except that failures are retried (by default, 3 times). - /// - [XunitTestCaseDiscoverer("Foundatio.Xunit.RetryFactDiscoverer", "Foundatio.TestHarness")] - public class RetryFactAttribute : FactAttribute + public RetryFactAttribute(int maxRetries = 3) { - public RetryFactAttribute(int maxRetries = 3) - { - MaxRetries = maxRetries; - } - - /// - /// Number of retries allowed for a failed test. If unset (or set less than 1), will - /// default to 3 attempts. - /// - public int MaxRetries { get; set; } + MaxRetries = maxRetries; } + + /// + /// Number of retries allowed for a failed test. If unset (or set less than 1), will + /// default to 3 attempts. + /// + public int MaxRetries { get; set; } } diff --git a/src/Foundatio.Xunit/Retry/RetryFactDiscoverer.cs b/src/Foundatio.Xunit/Retry/RetryFactDiscoverer.cs index 5e31ed09c..037c37a72 100644 --- a/src/Foundatio.Xunit/Retry/RetryFactDiscoverer.cs +++ b/src/Foundatio.Xunit/Retry/RetryFactDiscoverer.cs @@ -2,24 +2,23 @@ using Xunit.Abstractions; using Xunit.Sdk; -namespace Foundatio.Xunit +namespace Foundatio.Xunit; + +public class RetryFactDiscoverer : IXunitTestCaseDiscoverer { - public class RetryFactDiscoverer : IXunitTestCaseDiscoverer - { - readonly IMessageSink diagnosticMessageSink; + readonly IMessageSink diagnosticMessageSink; - public RetryFactDiscoverer(IMessageSink diagnosticMessageSink) - { - this.diagnosticMessageSink = diagnosticMessageSink; - } + public RetryFactDiscoverer(IMessageSink diagnosticMessageSink) + { + this.diagnosticMessageSink = diagnosticMessageSink; + } - public IEnumerable Discover(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute) - { - var maxRetries = factAttribute.GetNamedArgument("MaxRetries"); - if (maxRetries < 1) - maxRetries = 3; + public IEnumerable Discover(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute) + { + var maxRetries = factAttribute.GetNamedArgument("MaxRetries"); + if (maxRetries < 1) + maxRetries = 3; - yield return new RetryTestCase(diagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod, maxRetries); - } + yield return new RetryTestCase(diagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod, maxRetries); } } diff --git a/src/Foundatio.Xunit/Retry/RetryTestCase.cs b/src/Foundatio.Xunit/Retry/RetryTestCase.cs index 696c551c0..035165817 100644 --- a/src/Foundatio.Xunit/Retry/RetryTestCase.cs +++ b/src/Foundatio.Xunit/Retry/RetryTestCase.cs @@ -5,64 +5,63 @@ using Xunit.Abstractions; using Xunit.Sdk; -namespace Foundatio.Xunit +namespace Foundatio.Xunit; + +[Serializable] +public class RetryTestCase : XunitTestCase { - [Serializable] - public class RetryTestCase : XunitTestCase - { - private int maxRetries; + private int maxRetries; - [EditorBrowsable(EditorBrowsableState.Never)] - [Obsolete("Called by the de-serializer", true)] - public RetryTestCase() { } + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Called by the de-serializer", true)] + public RetryTestCase() { } - public RetryTestCase(IMessageSink diagnosticMessageSink, TestMethodDisplay testMethodDisplay, TestMethodDisplayOptions testMethodDisplayOptions, ITestMethod testMethod, int maxRetries) - : base(diagnosticMessageSink, testMethodDisplay, testMethodDisplayOptions, testMethod, testMethodArguments: null) - { - this.maxRetries = maxRetries; - } + public RetryTestCase(IMessageSink diagnosticMessageSink, TestMethodDisplay testMethodDisplay, TestMethodDisplayOptions testMethodDisplayOptions, ITestMethod testMethod, int maxRetries) + : base(diagnosticMessageSink, testMethodDisplay, testMethodDisplayOptions, testMethod, testMethodArguments: null) + { + this.maxRetries = maxRetries; + } + + // This method is called by the xUnit test framework classes to run the test case. We will do the + // loop here, forwarding on to the implementation in XunitTestCase to do the heavy lifting. We will + // continue to re-run the test until the aggregator has an error (meaning that some internal error + // condition happened), or the test runs without failure, or we've hit the maximum number of tries. + public override async Task RunAsync(IMessageSink diagnosticMessageSink, + IMessageBus messageBus, + object[] constructorArguments, + ExceptionAggregator aggregator, + CancellationTokenSource cancellationTokenSource) + { + var runCount = 0; - // This method is called by the xUnit test framework classes to run the test case. We will do the - // loop here, forwarding on to the implementation in XunitTestCase to do the heavy lifting. We will - // continue to re-run the test until the aggregator has an error (meaning that some internal error - // condition happened), or the test runs without failure, or we've hit the maximum number of tries. - public override async Task RunAsync(IMessageSink diagnosticMessageSink, - IMessageBus messageBus, - object[] constructorArguments, - ExceptionAggregator aggregator, - CancellationTokenSource cancellationTokenSource) + while (true) { - var runCount = 0; + // This is really the only tricky bit: we need to capture and delay messages (since those will + // contain run status) until we know we've decided to accept the final result; + var delayedMessageBus = new DelayedMessageBus(messageBus); - while (true) + var summary = await base.RunAsync(diagnosticMessageSink, delayedMessageBus, constructorArguments, aggregator, cancellationTokenSource); + if (aggregator.HasExceptions || summary.Failed == 0 || ++runCount >= maxRetries) { - // This is really the only tricky bit: we need to capture and delay messages (since those will - // contain run status) until we know we've decided to accept the final result; - var delayedMessageBus = new DelayedMessageBus(messageBus); - - var summary = await base.RunAsync(diagnosticMessageSink, delayedMessageBus, constructorArguments, aggregator, cancellationTokenSource); - if (aggregator.HasExceptions || summary.Failed == 0 || ++runCount >= maxRetries) - { - delayedMessageBus.Dispose(); // Sends all the delayed messages - return summary; - } - - diagnosticMessageSink.OnMessage(new DiagnosticMessage("Execution of '{0}' failed (attempt #{1}), retrying...", DisplayName, runCount)); + delayedMessageBus.Dispose(); // Sends all the delayed messages + return summary; } + + diagnosticMessageSink.OnMessage(new DiagnosticMessage("Execution of '{0}' failed (attempt #{1}), retrying...", DisplayName, runCount)); } + } - public override void Serialize(IXunitSerializationInfo data) - { - base.Serialize(data); + public override void Serialize(IXunitSerializationInfo data) + { + base.Serialize(data); - data.AddValue("MaxRetries", maxRetries); - } + data.AddValue("MaxRetries", maxRetries); + } - public override void Deserialize(IXunitSerializationInfo data) - { - base.Deserialize(data); + public override void Deserialize(IXunitSerializationInfo data) + { + base.Deserialize(data); - maxRetries = data.GetValue("MaxRetries"); - } + maxRetries = data.GetValue("MaxRetries"); } } diff --git a/src/Foundatio.Xunit/Retry/RetryTheoryAttribute.cs b/src/Foundatio.Xunit/Retry/RetryTheoryAttribute.cs index 6af4e038c..9e4ed951f 100644 --- a/src/Foundatio.Xunit/Retry/RetryTheoryAttribute.cs +++ b/src/Foundatio.Xunit/Retry/RetryTheoryAttribute.cs @@ -1,23 +1,22 @@ using Xunit; using Xunit.Sdk; -namespace Foundatio.Xunit +namespace Foundatio.Xunit; + +/// +/// Works just like [Fact] except that failures are retried (by default, 3 times). +/// +[XunitTestCaseDiscoverer("Foundatio.Xunit.RetryTheoryDiscoverer", "Foundatio.TestHarness")] +public class RetryTheoryAttribute : TheoryAttribute { - /// - /// Works just like [Fact] except that failures are retried (by default, 3 times). - /// - [XunitTestCaseDiscoverer("Foundatio.Xunit.RetryTheoryDiscoverer", "Foundatio.TestHarness")] - public class RetryTheoryAttribute : TheoryAttribute + public RetryTheoryAttribute(int maxRetries = 3) { - public RetryTheoryAttribute(int maxRetries = 3) - { - MaxRetries = maxRetries; - } - - /// - /// Number of retries allowed for a failed test. If unset (or set less than 1), will - /// default to 3 attempts. - /// - public int MaxRetries { get; set; } + MaxRetries = maxRetries; } + + /// + /// Number of retries allowed for a failed test. If unset (or set less than 1), will + /// default to 3 attempts. + /// + public int MaxRetries { get; set; } } diff --git a/src/Foundatio.Xunit/Retry/RetryTheoryDiscoverer.cs b/src/Foundatio.Xunit/Retry/RetryTheoryDiscoverer.cs index f9d8fc69f..62ade437b 100644 --- a/src/Foundatio.Xunit/Retry/RetryTheoryDiscoverer.cs +++ b/src/Foundatio.Xunit/Retry/RetryTheoryDiscoverer.cs @@ -2,24 +2,23 @@ using Xunit.Abstractions; using Xunit.Sdk; -namespace Foundatio.Xunit +namespace Foundatio.Xunit; + +public class RetryTheoryDiscoverer : IXunitTestCaseDiscoverer { - public class RetryTheoryDiscoverer : IXunitTestCaseDiscoverer - { - readonly IMessageSink diagnosticMessageSink; + readonly IMessageSink diagnosticMessageSink; - public RetryTheoryDiscoverer(IMessageSink diagnosticMessageSink) - { - this.diagnosticMessageSink = diagnosticMessageSink; - } + public RetryTheoryDiscoverer(IMessageSink diagnosticMessageSink) + { + this.diagnosticMessageSink = diagnosticMessageSink; + } - public IEnumerable Discover(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute) - { - var maxRetries = factAttribute.GetNamedArgument("MaxRetries"); - if (maxRetries < 1) - maxRetries = 3; + public IEnumerable Discover(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute) + { + var maxRetries = factAttribute.GetNamedArgument("MaxRetries"); + if (maxRetries < 1) + maxRetries = 3; - yield return new RetryTheoryTestCase(diagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod, maxRetries); - } + yield return new RetryTheoryTestCase(diagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod, maxRetries); } } diff --git a/src/Foundatio.Xunit/Retry/RetryTheoryTestCase.cs b/src/Foundatio.Xunit/Retry/RetryTheoryTestCase.cs index 0d04b03c3..3fe904867 100644 --- a/src/Foundatio.Xunit/Retry/RetryTheoryTestCase.cs +++ b/src/Foundatio.Xunit/Retry/RetryTheoryTestCase.cs @@ -5,64 +5,63 @@ using Xunit.Abstractions; using Xunit.Sdk; -namespace Foundatio.Xunit +namespace Foundatio.Xunit; + +[Serializable] +public class RetryTheoryTestCase : XunitTheoryTestCase { - [Serializable] - public class RetryTheoryTestCase : XunitTheoryTestCase - { - private int maxRetries; + private int maxRetries; - [EditorBrowsable(EditorBrowsableState.Never)] - [Obsolete("Called by the de-serializer", true)] - public RetryTheoryTestCase() { } + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Called by the de-serializer", true)] + public RetryTheoryTestCase() { } - public RetryTheoryTestCase(IMessageSink diagnosticMessageSink, TestMethodDisplay testMethodDisplay, TestMethodDisplayOptions testMethodDisplayOptions, ITestMethod testMethod, int maxRetries) - : base(diagnosticMessageSink, testMethodDisplay, testMethodDisplayOptions, testMethod) - { - this.maxRetries = maxRetries; - } + public RetryTheoryTestCase(IMessageSink diagnosticMessageSink, TestMethodDisplay testMethodDisplay, TestMethodDisplayOptions testMethodDisplayOptions, ITestMethod testMethod, int maxRetries) + : base(diagnosticMessageSink, testMethodDisplay, testMethodDisplayOptions, testMethod) + { + this.maxRetries = maxRetries; + } + + // This method is called by the xUnit test framework classes to run the test case. We will do the + // loop here, forwarding on to the implementation in XunitTestCase to do the heavy lifting. We will + // continue to re-run the test until the aggregator has an error (meaning that some internal error + // condition happened), or the test runs without failure, or we've hit the maximum number of tries. + public override async Task RunAsync(IMessageSink diagnosticMessageSink, + IMessageBus messageBus, + object[] constructorArguments, + ExceptionAggregator aggregator, + CancellationTokenSource cancellationTokenSource) + { + var runCount = 0; - // This method is called by the xUnit test framework classes to run the test case. We will do the - // loop here, forwarding on to the implementation in XunitTestCase to do the heavy lifting. We will - // continue to re-run the test until the aggregator has an error (meaning that some internal error - // condition happened), or the test runs without failure, or we've hit the maximum number of tries. - public override async Task RunAsync(IMessageSink diagnosticMessageSink, - IMessageBus messageBus, - object[] constructorArguments, - ExceptionAggregator aggregator, - CancellationTokenSource cancellationTokenSource) + while (true) { - var runCount = 0; + // This is really the only tricky bit: we need to capture and delay messages (since those will + // contain run status) until we know we've decided to accept the final result; + var delayedMessageBus = new DelayedMessageBus(messageBus); - while (true) + var summary = await base.RunAsync(diagnosticMessageSink, delayedMessageBus, constructorArguments, aggregator, cancellationTokenSource); + if (aggregator.HasExceptions || summary.Failed == 0 || ++runCount >= maxRetries) { - // This is really the only tricky bit: we need to capture and delay messages (since those will - // contain run status) until we know we've decided to accept the final result; - var delayedMessageBus = new DelayedMessageBus(messageBus); - - var summary = await base.RunAsync(diagnosticMessageSink, delayedMessageBus, constructorArguments, aggregator, cancellationTokenSource); - if (aggregator.HasExceptions || summary.Failed == 0 || ++runCount >= maxRetries) - { - delayedMessageBus.Dispose(); // Sends all the delayed messages - return summary; - } - - diagnosticMessageSink.OnMessage(new DiagnosticMessage("Execution of '{0}' failed (attempt #{1}), retrying...", DisplayName, runCount)); + delayedMessageBus.Dispose(); // Sends all the delayed messages + return summary; } + + diagnosticMessageSink.OnMessage(new DiagnosticMessage("Execution of '{0}' failed (attempt #{1}), retrying...", DisplayName, runCount)); } + } - public override void Serialize(IXunitSerializationInfo data) - { - base.Serialize(data); + public override void Serialize(IXunitSerializationInfo data) + { + base.Serialize(data); - data.AddValue("MaxRetries", maxRetries); - } + data.AddValue("MaxRetries", maxRetries); + } - public override void Deserialize(IXunitSerializationInfo data) - { - base.Deserialize(data); + public override void Deserialize(IXunitSerializationInfo data) + { + base.Deserialize(data); - maxRetries = data.GetValue("MaxRetries"); - } + maxRetries = data.GetValue("MaxRetries"); } } diff --git a/src/Foundatio/Caching/CacheValue.cs b/src/Foundatio/Caching/CacheValue.cs index 6631e9fb0..f0523278c 100644 --- a/src/Foundatio/Caching/CacheValue.cs +++ b/src/Foundatio/Caching/CacheValue.cs @@ -1,26 +1,25 @@ -namespace Foundatio.Caching +namespace Foundatio.Caching; + +public class CacheValue { - public class CacheValue + public CacheValue(T value, bool hasValue) { - public CacheValue(T value, bool hasValue) - { - Value = value; - HasValue = hasValue; - } + Value = value; + HasValue = hasValue; + } - public bool HasValue { get; } + public bool HasValue { get; } - public bool IsNull => Value == null; + public bool IsNull => Value == null; - public T Value { get; } + public T Value { get; } - public static CacheValue Null { get; } = new CacheValue(default, true); + public static CacheValue Null { get; } = new CacheValue(default, true); - public static CacheValue NoValue { get; } = new CacheValue(default, false); + public static CacheValue NoValue { get; } = new CacheValue(default, false); - public override string ToString() - { - return Value?.ToString() ?? ""; - } + public override string ToString() + { + return Value?.ToString() ?? ""; } } diff --git a/src/Foundatio/Caching/HybridCacheClient.cs b/src/Foundatio/Caching/HybridCacheClient.cs index 7513f9c76..b71bcd962 100644 --- a/src/Foundatio/Caching/HybridCacheClient.cs +++ b/src/Foundatio/Caching/HybridCacheClient.cs @@ -8,334 +8,333 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -namespace Foundatio.Caching +namespace Foundatio.Caching; + +public interface IHybridCacheClient : ICacheClient { } + +public class HybridCacheClient : IHybridCacheClient { - public interface IHybridCacheClient : ICacheClient { } + protected readonly ICacheClient _distributedCache; + protected readonly IMessageBus _messageBus; + private readonly string _cacheId = Guid.NewGuid().ToString("N"); + private readonly InMemoryCacheClient _localCache; + private readonly ILogger _logger; + private long _localCacheHits; + private long _invalidateCacheCalls; + + public HybridCacheClient(ICacheClient distributedCacheClient, IMessageBus messageBus, InMemoryCacheClientOptions localCacheOptions = null, ILoggerFactory loggerFactory = null) + { + _logger = loggerFactory?.CreateLogger() ?? NullLogger.Instance; + _distributedCache = distributedCacheClient; + _messageBus = messageBus; + _messageBus.SubscribeAsync(OnRemoteCacheItemExpiredAsync).AnyContext().GetAwaiter().GetResult(); + if (localCacheOptions == null) + localCacheOptions = new InMemoryCacheClientOptions { LoggerFactory = loggerFactory }; + _localCache = new InMemoryCacheClient(localCacheOptions); + _localCache.ItemExpired.AddHandler(OnLocalCacheItemExpiredAsync); + } + + public InMemoryCacheClient LocalCache => _localCache; + public long LocalCacheHits => _localCacheHits; + public long InvalidateCacheCalls => _invalidateCacheCalls; - public class HybridCacheClient : IHybridCacheClient + private Task OnLocalCacheItemExpiredAsync(object sender, ItemExpiredEventArgs args) { - protected readonly ICacheClient _distributedCache; - protected readonly IMessageBus _messageBus; - private readonly string _cacheId = Guid.NewGuid().ToString("N"); - private readonly InMemoryCacheClient _localCache; - private readonly ILogger _logger; - private long _localCacheHits; - private long _invalidateCacheCalls; - - public HybridCacheClient(ICacheClient distributedCacheClient, IMessageBus messageBus, InMemoryCacheClientOptions localCacheOptions = null, ILoggerFactory loggerFactory = null) - { - _logger = loggerFactory?.CreateLogger() ?? NullLogger.Instance; - _distributedCache = distributedCacheClient; - _messageBus = messageBus; - _messageBus.SubscribeAsync(OnRemoteCacheItemExpiredAsync).AnyContext().GetAwaiter().GetResult(); - if (localCacheOptions == null) - localCacheOptions = new InMemoryCacheClientOptions { LoggerFactory = loggerFactory }; - _localCache = new InMemoryCacheClient(localCacheOptions); - _localCache.ItemExpired.AddHandler(OnLocalCacheItemExpiredAsync); - } + if (!args.SendNotification) + return Task.CompletedTask; - public InMemoryCacheClient LocalCache => _localCache; - public long LocalCacheHits => _localCacheHits; - public long InvalidateCacheCalls => _invalidateCacheCalls; + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Local cache expired event: key={Key}", args.Key); + return _messageBus.PublishAsync(new InvalidateCache { CacheId = _cacheId, Keys = new[] { args.Key }, Expired = true }); + } - private Task OnLocalCacheItemExpiredAsync(object sender, ItemExpiredEventArgs args) - { - if (!args.SendNotification) - return Task.CompletedTask; + private Task OnRemoteCacheItemExpiredAsync(InvalidateCache message) + { + if (!String.IsNullOrEmpty(message.CacheId) && String.Equals(_cacheId, message.CacheId)) + return Task.CompletedTask; - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Local cache expired event: key={Key}", args.Key); - return _messageBus.PublishAsync(new InvalidateCache { CacheId = _cacheId, Keys = new[] { args.Key }, Expired = true }); + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Invalidating local cache from remote: id={CacheId} expired={Expired} keys={Keys}", message.CacheId, message.Expired, String.Join(",", message.Keys ?? new string[] { })); + Interlocked.Increment(ref _invalidateCacheCalls); + if (message.FlushAll) + { + _logger.LogTrace("Flushed local cache"); + return _localCache.RemoveAllAsync(); } - private Task OnRemoteCacheItemExpiredAsync(InvalidateCache message) + if (message.Keys != null && message.Keys.Length > 0) { - if (!String.IsNullOrEmpty(message.CacheId) && String.Equals(_cacheId, message.CacheId)) - return Task.CompletedTask; - - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Invalidating local cache from remote: id={CacheId} expired={Expired} keys={Keys}", message.CacheId, message.Expired, String.Join(",", message.Keys ?? new string[] { })); - Interlocked.Increment(ref _invalidateCacheCalls); - if (message.FlushAll) + var tasks = new List(message.Keys.Length); + var keysToRemove = new List(message.Keys.Length); + foreach (string key in message.Keys) { - _logger.LogTrace("Flushed local cache"); - return _localCache.RemoveAllAsync(); + if (message.Expired) + _localCache.RemoveExpiredKey(key, false); + else if (key.EndsWith("*")) + tasks.Add(_localCache.RemoveByPrefixAsync(key.Substring(0, key.Length - 1))); + else + keysToRemove.Add(key); } - if (message.Keys != null && message.Keys.Length > 0) - { - var tasks = new List(message.Keys.Length); - var keysToRemove = new List(message.Keys.Length); - foreach (string key in message.Keys) - { - if (message.Expired) - _localCache.RemoveExpiredKey(key, false); - else if (key.EndsWith("*")) - tasks.Add(_localCache.RemoveByPrefixAsync(key.Substring(0, key.Length - 1))); - else - keysToRemove.Add(key); - } - - if (keysToRemove.Count > 0) - tasks.Add(_localCache.RemoveAllAsync(keysToRemove)); - - return Task.WhenAll(tasks); - } + if (keysToRemove.Count > 0) + tasks.Add(_localCache.RemoveAllAsync(keysToRemove)); - _logger.LogWarning("Unknown invalidate cache message"); - return Task.CompletedTask; + return Task.WhenAll(tasks); } - public async Task RemoveAsync(string key) - { - var removed = await _distributedCache.RemoveAsync(key).AnyContext(); - await _localCache.RemoveAsync(key).AnyContext(); - await _messageBus.PublishAsync(new InvalidateCache { CacheId = _cacheId, Keys = new[] { key } }).AnyContext(); - return removed; - } + _logger.LogWarning("Unknown invalidate cache message"); + return Task.CompletedTask; + } - public async Task RemoveIfEqualAsync(string key, T expected) - { - var removed = await _distributedCache.RemoveAsync(key).AnyContext(); - await _localCache.RemoveAsync(key).AnyContext(); - await _messageBus.PublishAsync(new InvalidateCache { CacheId = _cacheId, Keys = new[] { key } }).AnyContext(); - return removed; - } + public async Task RemoveAsync(string key) + { + var removed = await _distributedCache.RemoveAsync(key).AnyContext(); + await _localCache.RemoveAsync(key).AnyContext(); + await _messageBus.PublishAsync(new InvalidateCache { CacheId = _cacheId, Keys = new[] { key } }).AnyContext(); + return removed; + } + + public async Task RemoveIfEqualAsync(string key, T expected) + { + var removed = await _distributedCache.RemoveAsync(key).AnyContext(); + await _localCache.RemoveAsync(key).AnyContext(); + await _messageBus.PublishAsync(new InvalidateCache { CacheId = _cacheId, Keys = new[] { key } }).AnyContext(); + return removed; + } + + public async Task RemoveAllAsync(IEnumerable keys = null) + { + var items = keys?.ToArray(); + bool flushAll = items == null || items.Length == 0; + var removed = await _distributedCache.RemoveAllAsync(items).AnyContext(); + await _localCache.RemoveAllAsync(items).AnyContext(); + await _messageBus.PublishAsync(new InvalidateCache { CacheId = _cacheId, FlushAll = flushAll, Keys = items }).AnyContext(); + return removed; + } + + public async Task RemoveByPrefixAsync(string prefix) + { + var removed = await _distributedCache.RemoveByPrefixAsync(prefix).AnyContext(); + await _localCache.RemoveByPrefixAsync(prefix).AnyContext(); + await _messageBus.PublishAsync(new InvalidateCache { CacheId = _cacheId, Keys = new[] { prefix + "*" } }).AnyContext(); + return removed; + } - public async Task RemoveAllAsync(IEnumerable keys = null) + public async Task> GetAsync(string key) + { + var cacheValue = await _localCache.GetAsync(key).AnyContext(); + if (cacheValue.HasValue) { - var items = keys?.ToArray(); - bool flushAll = items == null || items.Length == 0; - var removed = await _distributedCache.RemoveAllAsync(items).AnyContext(); - await _localCache.RemoveAllAsync(items).AnyContext(); - await _messageBus.PublishAsync(new InvalidateCache { CacheId = _cacheId, FlushAll = flushAll, Keys = items }).AnyContext(); - return removed; + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Local cache hit: {Key}", key); + Interlocked.Increment(ref _localCacheHits); + return cacheValue; } - public async Task RemoveByPrefixAsync(string prefix) + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Local cache miss: {Key}", key); + cacheValue = await _distributedCache.GetAsync(key).AnyContext(); + if (cacheValue.HasValue) { - var removed = await _distributedCache.RemoveByPrefixAsync(prefix).AnyContext(); - await _localCache.RemoveByPrefixAsync(prefix).AnyContext(); - await _messageBus.PublishAsync(new InvalidateCache { CacheId = _cacheId, Keys = new[] { prefix + "*" } }).AnyContext(); - return removed; + var expiration = await _distributedCache.GetExpirationAsync(key).AnyContext(); + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Setting Local cache key: {Key} with expiration: {Expiration}", key, expiration); + + await _localCache.SetAsync(key, cacheValue.Value, expiration).AnyContext(); + return cacheValue; } - public async Task> GetAsync(string key) - { - var cacheValue = await _localCache.GetAsync(key).AnyContext(); - if (cacheValue.HasValue) - { - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Local cache hit: {Key}", key); - Interlocked.Increment(ref _localCacheHits); - return cacheValue; - } + return cacheValue.HasValue ? cacheValue : CacheValue.NoValue; + } - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Local cache miss: {Key}", key); - cacheValue = await _distributedCache.GetAsync(key).AnyContext(); - if (cacheValue.HasValue) - { - var expiration = await _distributedCache.GetExpirationAsync(key).AnyContext(); - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Setting Local cache key: {Key} with expiration: {Expiration}", key, expiration); + public Task>> GetAllAsync(IEnumerable keys) + { + return _distributedCache.GetAllAsync(keys); + } - await _localCache.SetAsync(key, cacheValue.Value, expiration).AnyContext(); - return cacheValue; - } + public async Task AddAsync(string key, T value, TimeSpan? expiresIn = null) + { + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Adding key {Key} to local cache with expiration: {Expiration}", key, expiresIn); + bool added = await _distributedCache.AddAsync(key, value, expiresIn).AnyContext(); + if (added) + await _localCache.SetAsync(key, value, expiresIn).AnyContext(); - return cacheValue.HasValue ? cacheValue : CacheValue.NoValue; - } + return added; + } - public Task>> GetAllAsync(IEnumerable keys) - { - return _distributedCache.GetAllAsync(keys); - } + public async Task SetAsync(string key, T value, TimeSpan? expiresIn = null) + { + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Setting key {Key} to local cache with expiration: {Expiration}", key, expiresIn); + await _localCache.SetAsync(key, value, expiresIn).AnyContext(); + var set = await _distributedCache.SetAsync(key, value, expiresIn).AnyContext(); + await _messageBus.PublishAsync(new InvalidateCache { CacheId = _cacheId, Keys = new[] { key } }).AnyContext(); - public async Task AddAsync(string key, T value, TimeSpan? expiresIn = null) - { - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Adding key {Key} to local cache with expiration: {Expiration}", key, expiresIn); - bool added = await _distributedCache.AddAsync(key, value, expiresIn).AnyContext(); - if (added) - await _localCache.SetAsync(key, value, expiresIn).AnyContext(); + return set; + } - return added; - } + public async Task SetAllAsync(IDictionary values, TimeSpan? expiresIn = null) + { + if (values == null || values.Count == 0) + return 0; + + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Adding keys {Keys} to local cache with expiration: {Expiration}", values.Keys, expiresIn); + await _localCache.SetAllAsync(values, expiresIn).AnyContext(); + var set = await _distributedCache.SetAllAsync(values, expiresIn).AnyContext(); + await _messageBus.PublishAsync(new InvalidateCache { CacheId = _cacheId, Keys = values.Keys.ToArray() }).AnyContext(); + return set; + } - public async Task SetAsync(string key, T value, TimeSpan? expiresIn = null) - { - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Setting key {Key} to local cache with expiration: {Expiration}", key, expiresIn); - await _localCache.SetAsync(key, value, expiresIn).AnyContext(); - var set = await _distributedCache.SetAsync(key, value, expiresIn).AnyContext(); - await _messageBus.PublishAsync(new InvalidateCache { CacheId = _cacheId, Keys = new[] { key } }).AnyContext(); + public async Task ReplaceAsync(string key, T value, TimeSpan? expiresIn = null) + { + await _localCache.ReplaceAsync(key, value, expiresIn).AnyContext(); + bool replaced = await _distributedCache.ReplaceAsync(key, value, expiresIn).AnyContext(); + await _messageBus.PublishAsync(new InvalidateCache { CacheId = _cacheId, Keys = new[] { key } }).AnyContext(); + return replaced; + } - return set; - } + public async Task ReplaceIfEqualAsync(string key, T value, T expected, TimeSpan? expiresIn = null) + { + await _localCache.ReplaceAsync(key, value, expiresIn).AnyContext(); + bool replaced = await _distributedCache.ReplaceAsync(key, value, expiresIn).AnyContext(); + await _messageBus.PublishAsync(new InvalidateCache { CacheId = _cacheId, Keys = new[] { key } }).AnyContext(); + return replaced; + } - public async Task SetAllAsync(IDictionary values, TimeSpan? expiresIn = null) - { - if (values == null || values.Count == 0) - return 0; + public async Task IncrementAsync(string key, double amount, TimeSpan? expiresIn = null) + { + double incremented = await _distributedCache.IncrementAsync(key, amount, expiresIn).AnyContext(); + await _localCache.ReplaceAsync(key, incremented, expiresIn); + await _messageBus.PublishAsync(new InvalidateCache { CacheId = _cacheId, Keys = new[] { key } }).AnyContext(); + return incremented; + } - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Adding keys {Keys} to local cache with expiration: {Expiration}", values.Keys, expiresIn); - await _localCache.SetAllAsync(values, expiresIn).AnyContext(); - var set = await _distributedCache.SetAllAsync(values, expiresIn).AnyContext(); - await _messageBus.PublishAsync(new InvalidateCache { CacheId = _cacheId, Keys = values.Keys.ToArray() }).AnyContext(); - return set; - } + public async Task IncrementAsync(string key, long amount, TimeSpan? expiresIn = null) + { + long incremented = await _distributedCache.IncrementAsync(key, amount, expiresIn).AnyContext(); + await _localCache.ReplaceAsync(key, incremented, expiresIn); + await _messageBus.PublishAsync(new InvalidateCache { CacheId = _cacheId, Keys = new[] { key } }).AnyContext(); + return incremented; + } - public async Task ReplaceAsync(string key, T value, TimeSpan? expiresIn = null) - { - await _localCache.ReplaceAsync(key, value, expiresIn).AnyContext(); - bool replaced = await _distributedCache.ReplaceAsync(key, value, expiresIn).AnyContext(); - await _messageBus.PublishAsync(new InvalidateCache { CacheId = _cacheId, Keys = new[] { key } }).AnyContext(); - return replaced; - } + public Task ExistsAsync(string key) + { + return _distributedCache.ExistsAsync(key); + } - public async Task ReplaceIfEqualAsync(string key, T value, T expected, TimeSpan? expiresIn = null) - { - await _localCache.ReplaceAsync(key, value, expiresIn).AnyContext(); - bool replaced = await _distributedCache.ReplaceAsync(key, value, expiresIn).AnyContext(); - await _messageBus.PublishAsync(new InvalidateCache { CacheId = _cacheId, Keys = new[] { key } }).AnyContext(); - return replaced; - } + public Task GetExpirationAsync(string key) + { + return _distributedCache.GetExpirationAsync(key); + } - public async Task IncrementAsync(string key, double amount, TimeSpan? expiresIn = null) - { - double incremented = await _distributedCache.IncrementAsync(key, amount, expiresIn).AnyContext(); - await _localCache.ReplaceAsync(key, incremented, expiresIn); - await _messageBus.PublishAsync(new InvalidateCache { CacheId = _cacheId, Keys = new[] { key } }).AnyContext(); - return incremented; - } + public async Task SetExpirationAsync(string key, TimeSpan expiresIn) + { + await _localCache.SetExpirationAsync(key, expiresIn).AnyContext(); + await _distributedCache.SetExpirationAsync(key, expiresIn).AnyContext(); + await _messageBus.PublishAsync(new InvalidateCache { CacheId = _cacheId, Keys = new[] { key } }).AnyContext(); + } - public async Task IncrementAsync(string key, long amount, TimeSpan? expiresIn = null) - { - long incremented = await _distributedCache.IncrementAsync(key, amount, expiresIn).AnyContext(); - await _localCache.ReplaceAsync(key, incremented, expiresIn); - await _messageBus.PublishAsync(new InvalidateCache { CacheId = _cacheId, Keys = new[] { key } }).AnyContext(); - return incremented; - } + public async Task SetIfHigherAsync(string key, double value, TimeSpan? expiresIn = null) + { + await _localCache.RemoveAsync(key).AnyContext(); + double difference = await _distributedCache.SetIfHigherAsync(key, value, expiresIn).AnyContext(); + await _messageBus.PublishAsync(new InvalidateCache { CacheId = _cacheId, Keys = new[] { key } }).AnyContext(); + return difference; + } - public Task ExistsAsync(string key) - { - return _distributedCache.ExistsAsync(key); - } + public async Task SetIfHigherAsync(string key, long value, TimeSpan? expiresIn = null) + { + await _localCache.RemoveAsync(key).AnyContext(); + long difference = await _distributedCache.SetIfHigherAsync(key, value, expiresIn).AnyContext(); + await _messageBus.PublishAsync(new InvalidateCache { CacheId = _cacheId, Keys = new[] { key } }).AnyContext(); + return difference; + } - public Task GetExpirationAsync(string key) - { - return _distributedCache.GetExpirationAsync(key); - } + public async Task SetIfLowerAsync(string key, double value, TimeSpan? expiresIn = null) + { + await _localCache.RemoveAsync(key).AnyContext(); + double difference = await _distributedCache.SetIfLowerAsync(key, value, expiresIn).AnyContext(); + await _messageBus.PublishAsync(new InvalidateCache { CacheId = _cacheId, Keys = new[] { key } }).AnyContext(); + return difference; + } - public async Task SetExpirationAsync(string key, TimeSpan expiresIn) - { - await _localCache.SetExpirationAsync(key, expiresIn).AnyContext(); - await _distributedCache.SetExpirationAsync(key, expiresIn).AnyContext(); - await _messageBus.PublishAsync(new InvalidateCache { CacheId = _cacheId, Keys = new[] { key } }).AnyContext(); - } + public async Task SetIfLowerAsync(string key, long value, TimeSpan? expiresIn = null) + { + await _localCache.RemoveAsync(key).AnyContext(); + long difference = await _distributedCache.SetIfLowerAsync(key, value, expiresIn).AnyContext(); + await _messageBus.PublishAsync(new InvalidateCache { CacheId = _cacheId, Keys = new[] { key } }).AnyContext(); + return difference; + } - public async Task SetIfHigherAsync(string key, double value, TimeSpan? expiresIn = null) + public async Task ListAddAsync(string key, IEnumerable values, TimeSpan? expiresIn = null) + { + if (values is string stringValue) { - await _localCache.RemoveAsync(key).AnyContext(); - double difference = await _distributedCache.SetIfHigherAsync(key, value, expiresIn).AnyContext(); + await _localCache.ListAddAsync(key, stringValue, expiresIn).AnyContext(); + long set = await _distributedCache.ListAddAsync(key, stringValue, expiresIn).AnyContext(); await _messageBus.PublishAsync(new InvalidateCache { CacheId = _cacheId, Keys = new[] { key } }).AnyContext(); - return difference; + return set; } - - public async Task SetIfHigherAsync(string key, long value, TimeSpan? expiresIn = null) + else { - await _localCache.RemoveAsync(key).AnyContext(); - long difference = await _distributedCache.SetIfHigherAsync(key, value, expiresIn).AnyContext(); + var items = values?.ToArray(); + await _localCache.ListAddAsync(key, items, expiresIn).AnyContext(); + long set = await _distributedCache.ListAddAsync(key, items, expiresIn).AnyContext(); await _messageBus.PublishAsync(new InvalidateCache { CacheId = _cacheId, Keys = new[] { key } }).AnyContext(); - return difference; + return set; } + } - public async Task SetIfLowerAsync(string key, double value, TimeSpan? expiresIn = null) + public async Task ListRemoveAsync(string key, IEnumerable values, TimeSpan? expiresIn = null) + { + if (values is string stringValue) { - await _localCache.RemoveAsync(key).AnyContext(); - double difference = await _distributedCache.SetIfLowerAsync(key, value, expiresIn).AnyContext(); + await _localCache.ListRemoveAsync(key, stringValue, expiresIn).AnyContext(); + long removed = await _distributedCache.ListRemoveAsync(key, stringValue, expiresIn).AnyContext(); await _messageBus.PublishAsync(new InvalidateCache { CacheId = _cacheId, Keys = new[] { key } }).AnyContext(); - return difference; + return removed; } - - public async Task SetIfLowerAsync(string key, long value, TimeSpan? expiresIn = null) + else { - await _localCache.RemoveAsync(key).AnyContext(); - long difference = await _distributedCache.SetIfLowerAsync(key, value, expiresIn).AnyContext(); + var items = values?.ToArray(); + await _localCache.ListRemoveAsync(key, items, expiresIn).AnyContext(); + long removed = await _distributedCache.ListRemoveAsync(key, items, expiresIn).AnyContext(); await _messageBus.PublishAsync(new InvalidateCache { CacheId = _cacheId, Keys = new[] { key } }).AnyContext(); - return difference; - } - - public async Task ListAddAsync(string key, IEnumerable values, TimeSpan? expiresIn = null) - { - if (values is string stringValue) - { - await _localCache.ListAddAsync(key, stringValue, expiresIn).AnyContext(); - long set = await _distributedCache.ListAddAsync(key, stringValue, expiresIn).AnyContext(); - await _messageBus.PublishAsync(new InvalidateCache { CacheId = _cacheId, Keys = new[] { key } }).AnyContext(); - return set; - } - else - { - var items = values?.ToArray(); - await _localCache.ListAddAsync(key, items, expiresIn).AnyContext(); - long set = await _distributedCache.ListAddAsync(key, items, expiresIn).AnyContext(); - await _messageBus.PublishAsync(new InvalidateCache { CacheId = _cacheId, Keys = new[] { key } }).AnyContext(); - return set; - } + return removed; } + } - public async Task ListRemoveAsync(string key, IEnumerable values, TimeSpan? expiresIn = null) + public async Task>> GetListAsync(string key, int? page = null, int pageSize = 100) + { + var cacheValue = await _localCache.GetListAsync(key, page, pageSize).AnyContext(); + if (cacheValue.HasValue) { - if (values is string stringValue) - { - await _localCache.ListRemoveAsync(key, stringValue, expiresIn).AnyContext(); - long removed = await _distributedCache.ListRemoveAsync(key, stringValue, expiresIn).AnyContext(); - await _messageBus.PublishAsync(new InvalidateCache { CacheId = _cacheId, Keys = new[] { key } }).AnyContext(); - return removed; - } - else - { - var items = values?.ToArray(); - await _localCache.ListRemoveAsync(key, items, expiresIn).AnyContext(); - long removed = await _distributedCache.ListRemoveAsync(key, items, expiresIn).AnyContext(); - await _messageBus.PublishAsync(new InvalidateCache { CacheId = _cacheId, Keys = new[] { key } }).AnyContext(); - return removed; - } + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Local cache hit: {Key}", key); + Interlocked.Increment(ref _localCacheHits); + return cacheValue; } - public async Task>> GetListAsync(string key, int? page = null, int pageSize = 100) + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Local cache miss: {Key}", key); + cacheValue = await _distributedCache.GetListAsync(key, page, pageSize).AnyContext(); + if (cacheValue.HasValue) { - var cacheValue = await _localCache.GetListAsync(key, page, pageSize).AnyContext(); - if (cacheValue.HasValue) - { - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Local cache hit: {Key}", key); - Interlocked.Increment(ref _localCacheHits); - return cacheValue; - } - - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Local cache miss: {Key}", key); - cacheValue = await _distributedCache.GetListAsync(key, page, pageSize).AnyContext(); - if (cacheValue.HasValue) - { - var expiration = await _distributedCache.GetExpirationAsync(key).AnyContext(); - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Setting Local cache key: {Key} with expiration: {Expiration}", key, expiration); - - await _localCache.ListAddAsync(key, cacheValue.Value, expiration).AnyContext(); - return cacheValue; - } + var expiration = await _distributedCache.GetExpirationAsync(key).AnyContext(); + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Setting Local cache key: {Key} with expiration: {Expiration}", key, expiration); - return cacheValue.HasValue ? cacheValue : CacheValue>.NoValue; + await _localCache.ListAddAsync(key, cacheValue.Value, expiration).AnyContext(); + return cacheValue; } - public virtual void Dispose() - { - _localCache.ItemExpired.RemoveHandler(OnLocalCacheItemExpiredAsync); - _localCache.Dispose(); + return cacheValue.HasValue ? cacheValue : CacheValue>.NoValue; + } - // TODO: unsubscribe handler from messagebus. - } + public virtual void Dispose() + { + _localCache.ItemExpired.RemoveHandler(OnLocalCacheItemExpiredAsync); + _localCache.Dispose(); - public class InvalidateCache - { - public string CacheId { get; set; } - public string[] Keys { get; set; } - public bool FlushAll { get; set; } - public bool Expired { get; set; } - } + // TODO: unsubscribe handler from messagebus. + } + + public class InvalidateCache + { + public string CacheId { get; set; } + public string[] Keys { get; set; } + public bool FlushAll { get; set; } + public bool Expired { get; set; } } } diff --git a/src/Foundatio/Caching/ICacheClient.cs b/src/Foundatio/Caching/ICacheClient.cs index 51ecf8360..53b0b4370 100644 --- a/src/Foundatio/Caching/ICacheClient.cs +++ b/src/Foundatio/Caching/ICacheClient.cs @@ -2,32 +2,31 @@ using System.Collections.Generic; using System.Threading.Tasks; -namespace Foundatio.Caching +namespace Foundatio.Caching; + +public interface ICacheClient : IDisposable { - public interface ICacheClient : IDisposable - { - Task RemoveAsync(string key); - Task RemoveIfEqualAsync(string key, T expected); - Task RemoveAllAsync(IEnumerable keys = null); - Task RemoveByPrefixAsync(string prefix); - Task> GetAsync(string key); - Task>> GetAllAsync(IEnumerable keys); - Task AddAsync(string key, T value, TimeSpan? expiresIn = null); - Task SetAsync(string key, T value, TimeSpan? expiresIn = null); - Task SetAllAsync(IDictionary values, TimeSpan? expiresIn = null); - Task ReplaceAsync(string key, T value, TimeSpan? expiresIn = null); - Task ReplaceIfEqualAsync(string key, T value, T expected, TimeSpan? expiresIn = null); - Task IncrementAsync(string key, double amount, TimeSpan? expiresIn = null); - Task IncrementAsync(string key, long amount, TimeSpan? expiresIn = null); - Task ExistsAsync(string key); - Task GetExpirationAsync(string key); - Task SetExpirationAsync(string key, TimeSpan expiresIn); - Task SetIfHigherAsync(string key, double value, TimeSpan? expiresIn = null); - Task SetIfHigherAsync(string key, long value, TimeSpan? expiresIn = null); - Task SetIfLowerAsync(string key, double value, TimeSpan? expiresIn = null); - Task SetIfLowerAsync(string key, long value, TimeSpan? expiresIn = null); - Task ListAddAsync(string key, IEnumerable value, TimeSpan? expiresIn = null); - Task ListRemoveAsync(string key, IEnumerable value, TimeSpan? expiresIn = null); - Task>> GetListAsync(string key, int? page = null, int pageSize = 100); - } + Task RemoveAsync(string key); + Task RemoveIfEqualAsync(string key, T expected); + Task RemoveAllAsync(IEnumerable keys = null); + Task RemoveByPrefixAsync(string prefix); + Task> GetAsync(string key); + Task>> GetAllAsync(IEnumerable keys); + Task AddAsync(string key, T value, TimeSpan? expiresIn = null); + Task SetAsync(string key, T value, TimeSpan? expiresIn = null); + Task SetAllAsync(IDictionary values, TimeSpan? expiresIn = null); + Task ReplaceAsync(string key, T value, TimeSpan? expiresIn = null); + Task ReplaceIfEqualAsync(string key, T value, T expected, TimeSpan? expiresIn = null); + Task IncrementAsync(string key, double amount, TimeSpan? expiresIn = null); + Task IncrementAsync(string key, long amount, TimeSpan? expiresIn = null); + Task ExistsAsync(string key); + Task GetExpirationAsync(string key); + Task SetExpirationAsync(string key, TimeSpan expiresIn); + Task SetIfHigherAsync(string key, double value, TimeSpan? expiresIn = null); + Task SetIfHigherAsync(string key, long value, TimeSpan? expiresIn = null); + Task SetIfLowerAsync(string key, double value, TimeSpan? expiresIn = null); + Task SetIfLowerAsync(string key, long value, TimeSpan? expiresIn = null); + Task ListAddAsync(string key, IEnumerable value, TimeSpan? expiresIn = null); + Task ListRemoveAsync(string key, IEnumerable value, TimeSpan? expiresIn = null); + Task>> GetListAsync(string key, int? page = null, int pageSize = 100); } diff --git a/src/Foundatio/Caching/InMemoryCacheClient.cs b/src/Foundatio/Caching/InMemoryCacheClient.cs index 3e4cca051..a9d4aace6 100644 --- a/src/Foundatio/Caching/InMemoryCacheClient.cs +++ b/src/Foundatio/Caching/InMemoryCacheClient.cs @@ -10,984 +10,983 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -namespace Foundatio.Caching +namespace Foundatio.Caching; + +public class InMemoryCacheClient : ICacheClient { - public class InMemoryCacheClient : ICacheClient + private readonly ConcurrentDictionary _memory; + private bool _shouldClone; + private bool _shouldThrowOnSerializationErrors; + private int? _maxItems; + private long _writes; + private long _hits; + private long _misses; + private readonly ILogger _logger; + private readonly object _lock = new(); + + public InMemoryCacheClient() : this(o => o) { } + + public InMemoryCacheClient(InMemoryCacheClientOptions options = null) { - private readonly ConcurrentDictionary _memory; - private bool _shouldClone; - private bool _shouldThrowOnSerializationErrors; - private int? _maxItems; - private long _writes; - private long _hits; - private long _misses; - private readonly ILogger _logger; - private readonly object _lock = new(); + if (options == null) + options = new InMemoryCacheClientOptions(); + _shouldClone = options.CloneValues; + _shouldThrowOnSerializationErrors = options.ShouldThrowOnSerializationError; + _maxItems = options.MaxItems; + var loggerFactory = options.LoggerFactory ?? NullLoggerFactory.Instance; + _logger = loggerFactory.CreateLogger(); + _memory = new ConcurrentDictionary(); + } - public InMemoryCacheClient() : this(o => o) { } + public InMemoryCacheClient(Builder config) + : this(config(new InMemoryCacheClientOptionsBuilder()).Build()) { } - public InMemoryCacheClient(InMemoryCacheClientOptions options = null) - { - if (options == null) - options = new InMemoryCacheClientOptions(); - _shouldClone = options.CloneValues; - _shouldThrowOnSerializationErrors = options.ShouldThrowOnSerializationError; - _maxItems = options.MaxItems; - var loggerFactory = options.LoggerFactory ?? NullLoggerFactory.Instance; - _logger = loggerFactory.CreateLogger(); - _memory = new ConcurrentDictionary(); - } + public int Count => _memory.Count; + public int? MaxItems => _maxItems; + public long Calls => _writes + _hits + _misses; + public long Writes => _writes; + public long Reads => _hits + _misses; + public long Hits => _hits; + public long Misses => _misses; + + public override string ToString() + { + return $"Count: {Count} Calls: {Calls} Reads: {Reads} Writes: {Writes} Hits: {Hits} Misses: {Misses}"; + } - public InMemoryCacheClient(Builder config) - : this(config(new InMemoryCacheClientOptionsBuilder()).Build()) { } + public void ResetStats() + { + _writes = 0; + _hits = 0; + _misses = 0; + } + + public AsyncEvent ItemExpired { get; } = new AsyncEvent(); + + private void OnItemExpired(string key, bool sendNotification = true) + { + if (ItemExpired == null) + return; + + Task.Factory.StartNew(state => + { + var args = new ItemExpiredEventArgs + { + Client = this, + Key = key, + SendNotification = sendNotification + }; - public int Count => _memory.Count; - public int? MaxItems => _maxItems; - public long Calls => _writes + _hits + _misses; - public long Writes => _writes; - public long Reads => _hits + _misses; - public long Hits => _hits; - public long Misses => _misses; + return ItemExpired.InvokeAsync(this, args); + }, this, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + } - public override string ToString() + public ICollection Keys + { + get { - return $"Count: {Count} Calls: {Calls} Reads: {Reads} Writes: {Writes} Hits: {Hits} Misses: {Misses}"; + return _memory.ToArray() + .OrderBy(kvp => kvp.Value.LastAccessTicks) + .ThenBy(kvp => kvp.Value.InstanceNumber) + .Select(kvp => kvp.Key) + .ToList(); } + } - public void ResetStats() + public ICollection> Items + { + get { - _writes = 0; - _hits = 0; - _misses = 0; + return _memory.ToArray() + .OrderBy(kvp => kvp.Value.LastAccessTicks) + .ThenBy(kvp => kvp.Value.InstanceNumber) + .Select(kvp => new KeyValuePair(kvp.Key, kvp.Value)) + .ToList(); } + } - public AsyncEvent ItemExpired { get; } = new AsyncEvent(); + public Task RemoveAsync(string key) + { + if (String.IsNullOrEmpty(key)) + return Task.FromException(new ArgumentNullException(nameof(key), "Key cannot be null or empty.")); - private void OnItemExpired(string key, bool sendNotification = true) - { - if (ItemExpired == null) - return; + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("RemoveAsync: Removing key: {Key}", key); + return Task.FromResult(_memory.TryRemove(key, out _)); + } - Task.Factory.StartNew(state => - { - var args = new ItemExpiredEventArgs - { - Client = this, - Key = key, - SendNotification = sendNotification - }; + public async Task RemoveIfEqualAsync(string key, T expected) + { + if (String.IsNullOrEmpty(key)) + throw new ArgumentNullException(nameof(key), "Key cannot be null or empty"); - return ItemExpired.InvokeAsync(this, args); - }, this, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); - } + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.LogTrace("RemoveIfEqualAsync Key: {Key} Expected: {Expected}", key, expected); - public ICollection Keys + bool wasExpectedValue = false; + bool success = _memory.TryUpdate(key, (k, e) => { - get + var currentValue = e.GetValue(); + if (currentValue.Equals(expected)) { - return _memory.ToArray() - .OrderBy(kvp => kvp.Value.LastAccessTicks) - .ThenBy(kvp => kvp.Value.InstanceNumber) - .Select(kvp => kvp.Key) - .ToList(); + e.ExpiresAt = DateTime.MinValue; + wasExpectedValue = true; } - } - public ICollection> Items + return e; + }); + + success = success && wasExpectedValue; + + await StartMaintenanceAsync().AnyContext(); + + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.LogTrace("RemoveIfEqualAsync Key: {Key} Expected: {Expected} Success: {Success}", key, expected, success); + + return success; + } + + public Task RemoveAllAsync(IEnumerable keys = null) + { + if (keys == null) { - get - { - return _memory.ToArray() - .OrderBy(kvp => kvp.Value.LastAccessTicks) - .ThenBy(kvp => kvp.Value.InstanceNumber) - .Select(kvp => new KeyValuePair(kvp.Key, kvp.Value)) - .ToList(); - } + int count = _memory.Count; + _memory.Clear(); + return Task.FromResult(count); } - public Task RemoveAsync(string key) + int removed = 0; + foreach (string key in keys) { if (String.IsNullOrEmpty(key)) - return Task.FromException(new ArgumentNullException(nameof(key), "Key cannot be null or empty.")); + continue; - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("RemoveAsync: Removing key: {Key}", key); - return Task.FromResult(_memory.TryRemove(key, out _)); + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("RemoveAllAsync: Removing key: {Key}", key); + if (_memory.TryRemove(key, out _)) + removed++; } - public async Task RemoveIfEqualAsync(string key, T expected) - { - if (String.IsNullOrEmpty(key)) - throw new ArgumentNullException(nameof(key), "Key cannot be null or empty"); + return Task.FromResult(removed); + } - if (_logger.IsEnabled(LogLevel.Trace)) - _logger.LogTrace("RemoveIfEqualAsync Key: {Key} Expected: {Expected}", key, expected); + public Task RemoveByPrefixAsync(string prefix) + { + var keysToRemove = new List(); + var regex = new Regex(String.Concat(prefix, "*").Replace("*", ".*").Replace("?", ".+")); + try + { + foreach (string key in _memory.Keys.ToList()) + if (regex.IsMatch(key)) + keysToRemove.Add(key); + } + catch (Exception ex) + { + if (_logger.IsEnabled(LogLevel.Error)) + _logger.LogError(ex, "Error trying to remove items from cache with this {Prefix} prefix", prefix); + } - bool wasExpectedValue = false; - bool success = _memory.TryUpdate(key, (k, e) => - { - var currentValue = e.GetValue(); - if (currentValue.Equals(expected)) - { - e.ExpiresAt = DateTime.MinValue; - wasExpectedValue = true; - } + return RemoveAllAsync(keysToRemove); + } - return e; - }); + internal void RemoveExpiredKey(string key, bool sendNotification = true) + { + _logger.LogDebug("Removing expired cache entry {Key}", key); - success = success && wasExpectedValue; + if (_memory.TryRemove(key, out _)) + OnItemExpired(key, sendNotification); + } - await StartMaintenanceAsync().AnyContext(); + public Task> GetAsync(string key) + { + if (String.IsNullOrEmpty(key)) + throw new ArgumentNullException(nameof(key), "Key cannot be null or empty"); - if (_logger.IsEnabled(LogLevel.Trace)) - _logger.LogTrace("RemoveIfEqualAsync Key: {Key} Expected: {Expected} Success: {Success}", key, expected, success); + if (!_memory.TryGetValue(key, out var cacheEntry)) + { + Interlocked.Increment(ref _misses); + return Task.FromResult(CacheValue.NoValue); + } - return success; + if (cacheEntry.ExpiresAt < SystemClock.UtcNow) + { + RemoveExpiredKey(key); + Interlocked.Increment(ref _misses); + return Task.FromResult(CacheValue.NoValue); } - public Task RemoveAllAsync(IEnumerable keys = null) + Interlocked.Increment(ref _hits); + + try { - if (keys == null) - { - int count = _memory.Count; - _memory.Clear(); - return Task.FromResult(count); - } + var value = cacheEntry.GetValue(); + return Task.FromResult(new CacheValue(value, true)); + } + catch (Exception ex) + { + if (_logger.IsEnabled(LogLevel.Error)) + _logger.LogError(ex, "Unable to deserialize value {Value} to type {TypeFullName}", cacheEntry.Value, typeof(T).FullName); - int removed = 0; - foreach (string key in keys) - { - if (String.IsNullOrEmpty(key)) - continue; + if (_shouldThrowOnSerializationErrors) + throw; - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("RemoveAllAsync: Removing key: {Key}", key); - if (_memory.TryRemove(key, out _)) - removed++; - } + return Task.FromResult(CacheValue.NoValue); + } + } + + public async Task>> GetAllAsync(IEnumerable keys) + { + var map = new Dictionary>(); + + foreach (string key in keys) + map[key] = await GetAsync(key); + + return map; + } + + public Task AddAsync(string key, T value, TimeSpan? expiresIn = null) + { + if (String.IsNullOrEmpty(key)) + return Task.FromException(new ArgumentNullException(nameof(key), "Key cannot be null or empty.")); + + var expiresAt = expiresIn.HasValue ? SystemClock.UtcNow.SafeAdd(expiresIn.Value) : DateTime.MaxValue; + return SetInternalAsync(key, new CacheEntry(value, expiresAt, _shouldClone), true); + } + + public Task SetAsync(string key, T value, TimeSpan? expiresIn = null) + { + if (String.IsNullOrEmpty(key)) + return Task.FromException(new ArgumentNullException(nameof(key), "Key cannot be null or empty.")); - return Task.FromResult(removed); + var expiresAt = expiresIn.HasValue ? SystemClock.UtcNow.SafeAdd(expiresIn.Value) : DateTime.MaxValue; + return SetInternalAsync(key, new CacheEntry(value, expiresAt, _shouldClone)); + } + + public async Task SetIfHigherAsync(string key, double value, TimeSpan? expiresIn = null) + { + if (String.IsNullOrEmpty(key)) + throw new ArgumentNullException(nameof(key), "Key cannot be null or empty"); + + if (expiresIn?.Ticks < 0) + { + RemoveExpiredKey(key); + return -1; } - public Task RemoveByPrefixAsync(string prefix) + Interlocked.Increment(ref _writes); + + double difference = value; + var expiresAt = expiresIn.HasValue ? SystemClock.UtcNow.SafeAdd(expiresIn.Value) : DateTime.MaxValue; + _memory.AddOrUpdate(key, new CacheEntry(value, expiresAt, _shouldClone), (k, entry) => { - var keysToRemove = new List(); - var regex = new Regex(String.Concat(prefix, "*").Replace("*", ".*").Replace("?", ".+")); + double? currentValue = null; try { - foreach (string key in _memory.Keys.ToList()) - if (regex.IsMatch(key)) - keysToRemove.Add(key); + currentValue = entry.GetValue(); } catch (Exception ex) { - if (_logger.IsEnabled(LogLevel.Error)) - _logger.LogError(ex, "Error trying to remove items from cache with this {Prefix} prefix", prefix); + _logger.LogError(ex, "Unable to increment value, expected integer type"); } - return RemoveAllAsync(keysToRemove); - } + if (currentValue.HasValue && currentValue.Value < value) + { + difference = value - currentValue.Value; + entry.Value = value; + } + else + difference = 0; - internal void RemoveExpiredKey(string key, bool sendNotification = true) - { - _logger.LogDebug("Removing expired cache entry {Key}", key); + if (expiresIn.HasValue) + entry.ExpiresAt = expiresAt; - if (_memory.TryRemove(key, out _)) - OnItemExpired(key, sendNotification); - } + return entry; + }); - public Task> GetAsync(string key) - { - if (String.IsNullOrEmpty(key)) - throw new ArgumentNullException(nameof(key), "Key cannot be null or empty"); + await StartMaintenanceAsync().AnyContext(); - if (!_memory.TryGetValue(key, out var cacheEntry)) - { - Interlocked.Increment(ref _misses); - return Task.FromResult(CacheValue.NoValue); - } + return difference; + } - if (cacheEntry.ExpiresAt < SystemClock.UtcNow) - { - RemoveExpiredKey(key); - Interlocked.Increment(ref _misses); - return Task.FromResult(CacheValue.NoValue); - } + public async Task SetIfHigherAsync(string key, long value, TimeSpan? expiresIn = null) + { + if (String.IsNullOrEmpty(key)) + throw new ArgumentNullException(nameof(key), "Key cannot be null or empty"); + + if (expiresIn?.Ticks < 0) + { + RemoveExpiredKey(key); + return -1; + } - Interlocked.Increment(ref _hits); + Interlocked.Increment(ref _writes); + long difference = value; + var expiresAt = expiresIn.HasValue ? SystemClock.UtcNow.SafeAdd(expiresIn.Value) : DateTime.MaxValue; + _memory.AddOrUpdate(key, new CacheEntry(value, expiresAt, _shouldClone), (k, entry) => + { + long? currentValue = null; try { - var value = cacheEntry.GetValue(); - return Task.FromResult(new CacheValue(value, true)); + currentValue = entry.GetValue(); } catch (Exception ex) { - if (_logger.IsEnabled(LogLevel.Error)) - _logger.LogError(ex, "Unable to deserialize value {Value} to type {TypeFullName}", cacheEntry.Value, typeof(T).FullName); - - if (_shouldThrowOnSerializationErrors) - throw; + _logger.LogError(ex, "Unable to increment value, expected integer type"); + } - return Task.FromResult(CacheValue.NoValue); + if (currentValue.HasValue && currentValue.Value < value) + { + difference = value - currentValue.Value; + entry.Value = value; } - } + else + difference = 0; - public async Task>> GetAllAsync(IEnumerable keys) - { - var map = new Dictionary>(); + if (expiresIn.HasValue) + entry.ExpiresAt = expiresAt; - foreach (string key in keys) - map[key] = await GetAsync(key); + return entry; + }); - return map; - } + await StartMaintenanceAsync().AnyContext(); - public Task AddAsync(string key, T value, TimeSpan? expiresIn = null) - { - if (String.IsNullOrEmpty(key)) - return Task.FromException(new ArgumentNullException(nameof(key), "Key cannot be null or empty.")); + return difference; + } - var expiresAt = expiresIn.HasValue ? SystemClock.UtcNow.SafeAdd(expiresIn.Value) : DateTime.MaxValue; - return SetInternalAsync(key, new CacheEntry(value, expiresAt, _shouldClone), true); - } + public async Task SetIfLowerAsync(string key, double value, TimeSpan? expiresIn = null) + { + if (String.IsNullOrEmpty(key)) + throw new ArgumentNullException(nameof(key), "Key cannot be null or empty"); - public Task SetAsync(string key, T value, TimeSpan? expiresIn = null) + if (expiresIn?.Ticks < 0) { - if (String.IsNullOrEmpty(key)) - return Task.FromException(new ArgumentNullException(nameof(key), "Key cannot be null or empty.")); - - var expiresAt = expiresIn.HasValue ? SystemClock.UtcNow.SafeAdd(expiresIn.Value) : DateTime.MaxValue; - return SetInternalAsync(key, new CacheEntry(value, expiresAt, _shouldClone)); + RemoveExpiredKey(key); + return -1; } - public async Task SetIfHigherAsync(string key, double value, TimeSpan? expiresIn = null) - { - if (String.IsNullOrEmpty(key)) - throw new ArgumentNullException(nameof(key), "Key cannot be null or empty"); + Interlocked.Increment(ref _writes); - if (expiresIn?.Ticks < 0) + double difference = value; + var expiresAt = expiresIn.HasValue ? SystemClock.UtcNow.SafeAdd(expiresIn.Value) : DateTime.MaxValue; + _memory.AddOrUpdate(key, new CacheEntry(value, expiresAt, _shouldClone), (k, entry) => + { + double? currentValue = null; + try { - RemoveExpiredKey(key); - return -1; + currentValue = entry.GetValue(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Unable to increment value, expected integer type"); } - Interlocked.Increment(ref _writes); - - double difference = value; - var expiresAt = expiresIn.HasValue ? SystemClock.UtcNow.SafeAdd(expiresIn.Value) : DateTime.MaxValue; - _memory.AddOrUpdate(key, new CacheEntry(value, expiresAt, _shouldClone), (k, entry) => + if (currentValue.HasValue && currentValue.Value > value) { - double? currentValue = null; - try - { - currentValue = entry.GetValue(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Unable to increment value, expected integer type"); - } + difference = currentValue.Value - value; + entry.Value = value; + } + else + difference = 0; - if (currentValue.HasValue && currentValue.Value < value) - { - difference = value - currentValue.Value; - entry.Value = value; - } - else - difference = 0; + if (expiresIn.HasValue) + entry.ExpiresAt = expiresAt; - if (expiresIn.HasValue) - entry.ExpiresAt = expiresAt; + return entry; + }); - return entry; - }); + await StartMaintenanceAsync().AnyContext(); - await StartMaintenanceAsync().AnyContext(); + return difference; + } - return difference; - } + public async Task SetIfLowerAsync(string key, long value, TimeSpan? expiresIn = null) + { + if (String.IsNullOrEmpty(key)) + throw new ArgumentNullException(nameof(key), "Key cannot be null or empty"); - public async Task SetIfHigherAsync(string key, long value, TimeSpan? expiresIn = null) + if (expiresIn?.Ticks < 0) { - if (String.IsNullOrEmpty(key)) - throw new ArgumentNullException(nameof(key), "Key cannot be null or empty"); + RemoveExpiredKey(key); + return -1; + } - if (expiresIn?.Ticks < 0) + long difference = value; + var expiresAt = expiresIn.HasValue ? SystemClock.UtcNow.SafeAdd(expiresIn.Value) : DateTime.MaxValue; + _memory.AddOrUpdate(key, new CacheEntry(value, expiresAt, _shouldClone), (k, entry) => + { + long? currentValue = null; + try { - RemoveExpiredKey(key); - return -1; + currentValue = entry.GetValue(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Unable to increment value, expected integer type"); } - Interlocked.Increment(ref _writes); - - long difference = value; - var expiresAt = expiresIn.HasValue ? SystemClock.UtcNow.SafeAdd(expiresIn.Value) : DateTime.MaxValue; - _memory.AddOrUpdate(key, new CacheEntry(value, expiresAt, _shouldClone), (k, entry) => + if (currentValue.HasValue && currentValue.Value > value) { - long? currentValue = null; - try - { - currentValue = entry.GetValue(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Unable to increment value, expected integer type"); - } + difference = currentValue.Value - value; + entry.Value = value; + } + else + difference = 0; - if (currentValue.HasValue && currentValue.Value < value) - { - difference = value - currentValue.Value; - entry.Value = value; - } - else - difference = 0; + if (expiresIn.HasValue) + entry.ExpiresAt = expiresAt; - if (expiresIn.HasValue) - entry.ExpiresAt = expiresAt; + return entry; + }); - return entry; - }); + await StartMaintenanceAsync().AnyContext(); - await StartMaintenanceAsync().AnyContext(); + return difference; + } - return difference; - } + public async Task ListAddAsync(string key, IEnumerable values, TimeSpan? expiresIn = null) + { + if (String.IsNullOrEmpty(key)) + throw new ArgumentNullException(nameof(key), "Key cannot be null or empty"); - public async Task SetIfLowerAsync(string key, double value, TimeSpan? expiresIn = null) - { - if (String.IsNullOrEmpty(key)) - throw new ArgumentNullException(nameof(key), "Key cannot be null or empty"); + if (values == null) + throw new ArgumentNullException(nameof(values)); - if (expiresIn?.Ticks < 0) - { - RemoveExpiredKey(key); - return -1; - } + var expiresAt = expiresIn.HasValue ? SystemClock.UtcNow.SafeAdd(expiresIn.Value) : DateTime.MaxValue; + if (expiresAt < SystemClock.UtcNow) + { + RemoveExpiredKey(key); + return default; + } - Interlocked.Increment(ref _writes); + Interlocked.Increment(ref _writes); - double difference = value; - var expiresAt = expiresIn.HasValue ? SystemClock.UtcNow.SafeAdd(expiresIn.Value) : DateTime.MaxValue; - _memory.AddOrUpdate(key, new CacheEntry(value, expiresAt, _shouldClone), (k, entry) => + if (values is string stringValue) + { + var items = new HashSet(new[] { stringValue }); + var entry = new CacheEntry(items, expiresAt, _shouldClone); + _memory.AddOrUpdate(key, entry, (k, cacheEntry) => { - double? currentValue = null; - try - { - currentValue = entry.GetValue(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Unable to increment value, expected integer type"); - } + if (!(cacheEntry.Value is ICollection collection)) + throw new InvalidOperationException($"Unable to add value for key: {key}. Cache value does not contain a set"); - if (currentValue.HasValue && currentValue.Value > value) - { - difference = currentValue.Value - value; - entry.Value = value; - } - else - difference = 0; + collection.Add(stringValue); + cacheEntry.Value = collection; if (expiresIn.HasValue) - entry.ExpiresAt = expiresAt; + cacheEntry.ExpiresAt = expiresAt; - return entry; + return cacheEntry; }); await StartMaintenanceAsync().AnyContext(); - return difference; + return items.Count; } - - public async Task SetIfLowerAsync(string key, long value, TimeSpan? expiresIn = null) + else { - if (String.IsNullOrEmpty(key)) - throw new ArgumentNullException(nameof(key), "Key cannot be null or empty"); - - if (expiresIn?.Ticks < 0) - { - RemoveExpiredKey(key); - return -1; - } - - long difference = value; - var expiresAt = expiresIn.HasValue ? SystemClock.UtcNow.SafeAdd(expiresIn.Value) : DateTime.MaxValue; - _memory.AddOrUpdate(key, new CacheEntry(value, expiresAt, _shouldClone), (k, entry) => + var items = new HashSet(values); + var entry = new CacheEntry(items, expiresAt, _shouldClone); + _memory.AddOrUpdate(key, entry, (k, cacheEntry) => { - long? currentValue = null; - try - { - currentValue = entry.GetValue(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Unable to increment value, expected integer type"); - } + if (!(cacheEntry.Value is ICollection collection)) + throw new InvalidOperationException($"Unable to add value for key: {key}. Cache value does not contain a set"); - if (currentValue.HasValue && currentValue.Value > value) - { - difference = currentValue.Value - value; - entry.Value = value; - } - else - difference = 0; + collection.AddRange(items); + cacheEntry.Value = collection; if (expiresIn.HasValue) - entry.ExpiresAt = expiresAt; + cacheEntry.ExpiresAt = expiresAt; - return entry; + return cacheEntry; }); await StartMaintenanceAsync().AnyContext(); - return difference; + return items.Count; } + } - public async Task ListAddAsync(string key, IEnumerable values, TimeSpan? expiresIn = null) - { - if (String.IsNullOrEmpty(key)) - throw new ArgumentNullException(nameof(key), "Key cannot be null or empty"); + public Task ListRemoveAsync(string key, IEnumerable values, TimeSpan? expiresIn = null) + { + if (String.IsNullOrEmpty(key)) + throw new ArgumentNullException(nameof(key), "Key cannot be null or empty"); - if (values == null) - throw new ArgumentNullException(nameof(values)); + if (values == null) + throw new ArgumentNullException(nameof(values)); - var expiresAt = expiresIn.HasValue ? SystemClock.UtcNow.SafeAdd(expiresIn.Value) : DateTime.MaxValue; - if (expiresAt < SystemClock.UtcNow) - { - RemoveExpiredKey(key); - return default; - } + var expiresAt = expiresIn.HasValue ? SystemClock.UtcNow.SafeAdd(expiresIn.Value) : DateTime.MaxValue; + if (expiresAt < SystemClock.UtcNow) + { + RemoveExpiredKey(key); + return default; + } - Interlocked.Increment(ref _writes); + Interlocked.Increment(ref _writes); - if (values is string stringValue) + if (values is string stringValue) + { + var items = new HashSet(new[] { stringValue }); + _memory.TryUpdate(key, (k, cacheEntry) => { - var items = new HashSet(new[] { stringValue }); - var entry = new CacheEntry(items, expiresAt, _shouldClone); - _memory.AddOrUpdate(key, entry, (k, cacheEntry) => + if (cacheEntry.Value is ICollection collection && collection.Count > 0) { - if (!(cacheEntry.Value is ICollection collection)) - throw new InvalidOperationException($"Unable to add value for key: {key}. Cache value does not contain a set"); + foreach (var value in items) + collection.Remove(value); - collection.Add(stringValue); cacheEntry.Value = collection; + } - if (expiresIn.HasValue) - cacheEntry.ExpiresAt = expiresAt; - - return cacheEntry; - }); + if (expiresIn.HasValue) + cacheEntry.ExpiresAt = expiresAt; - await StartMaintenanceAsync().AnyContext(); + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Removed value from set with cache key: {Key}", key); + return cacheEntry; + }); - return items.Count; - } - else + return Task.FromResult(items.Count); + } + else + { + var items = new HashSet(values); + _memory.TryUpdate(key, (k, cacheEntry) => { - var items = new HashSet(values); - var entry = new CacheEntry(items, expiresAt, _shouldClone); - _memory.AddOrUpdate(key, entry, (k, cacheEntry) => + if (cacheEntry.Value is ICollection collection && collection.Count > 0) { - if (!(cacheEntry.Value is ICollection collection)) - throw new InvalidOperationException($"Unable to add value for key: {key}. Cache value does not contain a set"); + foreach (var value in items) + collection.Remove(value); - collection.AddRange(items); cacheEntry.Value = collection; + } - if (expiresIn.HasValue) - cacheEntry.ExpiresAt = expiresAt; - - return cacheEntry; - }); + if (expiresIn.HasValue) + cacheEntry.ExpiresAt = expiresAt; - await StartMaintenanceAsync().AnyContext(); + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Removed value from set with cache key: {Key}", key); + return cacheEntry; + }); - return items.Count; - } + return Task.FromResult(items.Count); } + } - public Task ListRemoveAsync(string key, IEnumerable values, TimeSpan? expiresIn = null) - { - if (String.IsNullOrEmpty(key)) - throw new ArgumentNullException(nameof(key), "Key cannot be null or empty"); - - if (values == null) - throw new ArgumentNullException(nameof(values)); - - var expiresAt = expiresIn.HasValue ? SystemClock.UtcNow.SafeAdd(expiresIn.Value) : DateTime.MaxValue; - if (expiresAt < SystemClock.UtcNow) - { - RemoveExpiredKey(key); - return default; - } + public async Task>> GetListAsync(string key, int? page = null, int pageSize = 100) + { + if (String.IsNullOrEmpty(key)) + throw new ArgumentNullException(nameof(key), "Key cannot be null or empty"); - Interlocked.Increment(ref _writes); + var list = await GetAsync>(key); + if (!list.HasValue || !page.HasValue) + return list; - if (values is string stringValue) - { - var items = new HashSet(new[] { stringValue }); - _memory.TryUpdate(key, (k, cacheEntry) => - { - if (cacheEntry.Value is ICollection collection && collection.Count > 0) - { - foreach (var value in items) - collection.Remove(value); + int skip = (page.Value - 1) * pageSize; + var pagedItems = list.Value.Skip(skip).Take(pageSize).ToArray(); + return new CacheValue>(pagedItems, true); + } - cacheEntry.Value = collection; - } + private async Task SetInternalAsync(string key, CacheEntry entry, bool addOnly = false) + { + if (String.IsNullOrEmpty(key)) + throw new ArgumentNullException(nameof(key), "SetInternalAsync: Key cannot be null or empty"); - if (expiresIn.HasValue) - cacheEntry.ExpiresAt = expiresAt; + if (entry.ExpiresAt < SystemClock.UtcNow) + { + RemoveExpiredKey(key); + return false; + } - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Removed value from set with cache key: {Key}", key); - return cacheEntry; - }); + Interlocked.Increment(ref _writes); - return Task.FromResult(items.Count); - } - else + if (addOnly) + { + if (!_memory.TryAdd(key, entry)) { - var items = new HashSet(values); - _memory.TryUpdate(key, (k, cacheEntry) => + + // check to see if existing entry is expired + bool updated = false; + _memory.TryUpdate(key, (key, existingEntry) => { - if (cacheEntry.Value is ICollection collection && collection.Count > 0) + if (existingEntry.ExpiresAt < SystemClock.UtcNow) { - foreach (var value in items) - collection.Remove(value); - - cacheEntry.Value = collection; + updated = true; + return entry; } - if (expiresIn.HasValue) - cacheEntry.ExpiresAt = expiresAt; - - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Removed value from set with cache key: {Key}", key); - return cacheEntry; + return existingEntry; }); - return Task.FromResult(items.Count); + if (!updated) + return false; } - } - - public async Task>> GetListAsync(string key, int? page = null, int pageSize = 100) - { - if (String.IsNullOrEmpty(key)) - throw new ArgumentNullException(nameof(key), "Key cannot be null or empty"); - var list = await GetAsync>(key); - if (!list.HasValue || !page.HasValue) - return list; - - int skip = (page.Value - 1) * pageSize; - var pagedItems = list.Value.Skip(skip).Take(pageSize).ToArray(); - return new CacheValue>(pagedItems, true); + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Added cache key: {Key}", key); } - - private async Task SetInternalAsync(string key, CacheEntry entry, bool addOnly = false) + else { - if (String.IsNullOrEmpty(key)) - throw new ArgumentNullException(nameof(key), "SetInternalAsync: Key cannot be null or empty"); - - if (entry.ExpiresAt < SystemClock.UtcNow) - { - RemoveExpiredKey(key); - return false; - } - - Interlocked.Increment(ref _writes); - - if (addOnly) - { - if (!_memory.TryAdd(key, entry)) - { - - // check to see if existing entry is expired - bool updated = false; - _memory.TryUpdate(key, (key, existingEntry) => - { - if (existingEntry.ExpiresAt < SystemClock.UtcNow) - { - updated = true; - return entry; - } + _memory.AddOrUpdate(key, entry, (k, cacheEntry) => entry); + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Set cache key: {Key}", key); + } - return existingEntry; - }); + await StartMaintenanceAsync(true).AnyContext(); - if (!updated) - return false; - } + return true; + } - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Added cache key: {Key}", key); - } - else - { - _memory.AddOrUpdate(key, entry, (k, cacheEntry) => entry); - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Set cache key: {Key}", key); - } + public async Task SetAllAsync(IDictionary values, TimeSpan? expiresIn = null) + { + if (values == null || values.Count == 0) + return 0; - await StartMaintenanceAsync(true).AnyContext(); + var tasks = new List>(); + foreach (var entry in values) + tasks.Add(SetAsync(entry.Key, entry.Value, expiresIn)); - return true; - } + bool[] results = await Task.WhenAll(tasks).AnyContext(); + return results.Count(r => r); + } - public async Task SetAllAsync(IDictionary values, TimeSpan? expiresIn = null) - { - if (values == null || values.Count == 0) - return 0; + public Task ReplaceAsync(string key, T value, TimeSpan? expiresIn = null) + { + if (String.IsNullOrEmpty(key)) + return Task.FromException(new ArgumentNullException(nameof(key), "Key cannot be null or empty.")); - var tasks = new List>(); - foreach (var entry in values) - tasks.Add(SetAsync(entry.Key, entry.Value, expiresIn)); + if (!_memory.ContainsKey(key)) + return Task.FromResult(false); - bool[] results = await Task.WhenAll(tasks).AnyContext(); - return results.Count(r => r); - } + return SetAsync(key, value, expiresIn); + } - public Task ReplaceAsync(string key, T value, TimeSpan? expiresIn = null) - { - if (String.IsNullOrEmpty(key)) - return Task.FromException(new ArgumentNullException(nameof(key), "Key cannot be null or empty.")); + public async Task ReplaceIfEqualAsync(string key, T value, T expected, TimeSpan? expiresIn = null) + { + if (String.IsNullOrEmpty(key)) + throw new ArgumentNullException(nameof(key), "Key cannot be null or empty"); - if (!_memory.ContainsKey(key)) - return Task.FromResult(false); + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.LogTrace("ReplaceIfEqualAsync Key: {Key} Expected: {Expected}", key, expected); - return SetAsync(key, value, expiresIn); - } + Interlocked.Increment(ref _writes); - public async Task ReplaceIfEqualAsync(string key, T value, T expected, TimeSpan? expiresIn = null) + var expiresAt = expiresIn.HasValue ? SystemClock.UtcNow.SafeAdd(expiresIn.Value) : DateTime.MaxValue; + bool wasExpectedValue = false; + bool success = _memory.TryUpdate(key, (k, cacheEntry) => { - if (String.IsNullOrEmpty(key)) - throw new ArgumentNullException(nameof(key), "Key cannot be null or empty"); + var currentValue = cacheEntry.GetValue(); + if (currentValue.Equals(expected)) + { + cacheEntry.Value = value; + wasExpectedValue = true; - if (_logger.IsEnabled(LogLevel.Trace)) - _logger.LogTrace("ReplaceIfEqualAsync Key: {Key} Expected: {Expected}", key, expected); + if (expiresIn.HasValue) + cacheEntry.ExpiresAt = expiresAt; + } - Interlocked.Increment(ref _writes); + return cacheEntry; + }); - var expiresAt = expiresIn.HasValue ? SystemClock.UtcNow.SafeAdd(expiresIn.Value) : DateTime.MaxValue; - bool wasExpectedValue = false; - bool success = _memory.TryUpdate(key, (k, cacheEntry) => - { - var currentValue = cacheEntry.GetValue(); - if (currentValue.Equals(expected)) - { - cacheEntry.Value = value; - wasExpectedValue = true; + success = success && wasExpectedValue; + await StartMaintenanceAsync().AnyContext(); - if (expiresIn.HasValue) - cacheEntry.ExpiresAt = expiresAt; - } + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.LogTrace("ReplaceIfEqualAsync Key: {Key} Expected: {Expected} Success: {Success}", key, expected, success); - return cacheEntry; - }); - - success = success && wasExpectedValue; - await StartMaintenanceAsync().AnyContext(); + return success; + } - if (_logger.IsEnabled(LogLevel.Trace)) - _logger.LogTrace("ReplaceIfEqualAsync Key: {Key} Expected: {Expected} Success: {Success}", key, expected, success); + public async Task IncrementAsync(string key, double amount, TimeSpan? expiresIn = null) + { + if (String.IsNullOrEmpty(key)) + throw new ArgumentNullException(nameof(key), "Key cannot be null or empty"); - return success; + if (expiresIn?.Ticks < 0) + { + RemoveExpiredKey(key); + return -1; } - public async Task IncrementAsync(string key, double amount, TimeSpan? expiresIn = null) - { - if (String.IsNullOrEmpty(key)) - throw new ArgumentNullException(nameof(key), "Key cannot be null or empty"); + Interlocked.Increment(ref _writes); - if (expiresIn?.Ticks < 0) + var expiresAt = expiresIn.HasValue ? SystemClock.UtcNow.SafeAdd(expiresIn.Value) : DateTime.MaxValue; + var result = _memory.AddOrUpdate(key, new CacheEntry(amount, expiresAt, _shouldClone), (k, entry) => + { + double? currentValue = null; + try + { + currentValue = entry.GetValue(); + } + catch (Exception ex) { - RemoveExpiredKey(key); - return -1; + _logger.LogError(ex, "Unable to increment value, expected integer type"); } - Interlocked.Increment(ref _writes); + if (currentValue.HasValue) + entry.Value = currentValue.Value + amount; + else + entry.Value = amount; - var expiresAt = expiresIn.HasValue ? SystemClock.UtcNow.SafeAdd(expiresIn.Value) : DateTime.MaxValue; - var result = _memory.AddOrUpdate(key, new CacheEntry(amount, expiresAt, _shouldClone), (k, entry) => - { - double? currentValue = null; - try - { - currentValue = entry.GetValue(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Unable to increment value, expected integer type"); - } + if (expiresIn.HasValue) + entry.ExpiresAt = expiresAt; - if (currentValue.HasValue) - entry.Value = currentValue.Value + amount; - else - entry.Value = amount; + return entry; + }); - if (expiresIn.HasValue) - entry.ExpiresAt = expiresAt; + await StartMaintenanceAsync().AnyContext(); - return entry; - }); + return result.GetValue(); + } - await StartMaintenanceAsync().AnyContext(); + public async Task IncrementAsync(string key, long amount, TimeSpan? expiresIn = null) + { + if (String.IsNullOrEmpty(key)) + throw new ArgumentNullException(nameof(key), "Key cannot be null or empty"); - return result.GetValue(); + if (expiresIn?.Ticks < 0) + { + RemoveExpiredKey(key); + return -1; } - public async Task IncrementAsync(string key, long amount, TimeSpan? expiresIn = null) - { - if (String.IsNullOrEmpty(key)) - throw new ArgumentNullException(nameof(key), "Key cannot be null or empty"); + Interlocked.Increment(ref _writes); - if (expiresIn?.Ticks < 0) + var expiresAt = expiresIn.HasValue ? SystemClock.UtcNow.SafeAdd(expiresIn.Value) : DateTime.MaxValue; + var result = _memory.AddOrUpdate(key, new CacheEntry(amount, expiresAt, _shouldClone), (k, entry) => + { + long? currentValue = null; + try + { + currentValue = entry.GetValue(); + } + catch (Exception ex) { - RemoveExpiredKey(key); - return -1; + _logger.LogError(ex, "Unable to increment value, expected integer type"); } - Interlocked.Increment(ref _writes); + if (currentValue.HasValue) + entry.Value = currentValue.Value + amount; + else + entry.Value = amount; - var expiresAt = expiresIn.HasValue ? SystemClock.UtcNow.SafeAdd(expiresIn.Value) : DateTime.MaxValue; - var result = _memory.AddOrUpdate(key, new CacheEntry(amount, expiresAt, _shouldClone), (k, entry) => - { - long? currentValue = null; - try - { - currentValue = entry.GetValue(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Unable to increment value, expected integer type"); - } + if (expiresIn.HasValue) + entry.ExpiresAt = expiresAt; - if (currentValue.HasValue) - entry.Value = currentValue.Value + amount; - else - entry.Value = amount; + return entry; + }); - if (expiresIn.HasValue) - entry.ExpiresAt = expiresAt; + await StartMaintenanceAsync().AnyContext(); - return entry; - }); + return result.GetValue(); + } - await StartMaintenanceAsync().AnyContext(); + public Task ExistsAsync(string key) + { + if (String.IsNullOrEmpty(key)) + return Task.FromException(new ArgumentNullException(nameof(key), "Key cannot be null or empty.")); - return result.GetValue(); + if (!_memory.TryGetValue(key, out var cacheEntry)) + { + Interlocked.Increment(ref _misses); + return Task.FromResult(false); } - public Task ExistsAsync(string key) - { - if (String.IsNullOrEmpty(key)) - return Task.FromException(new ArgumentNullException(nameof(key), "Key cannot be null or empty.")); + Interlocked.Increment(ref _hits); + if (cacheEntry.ExpiresAt < SystemClock.UtcNow) + return Task.FromResult(false); - if (!_memory.TryGetValue(key, out var cacheEntry)) - { - Interlocked.Increment(ref _misses); - return Task.FromResult(false); - } + return Task.FromResult(true); + } - Interlocked.Increment(ref _hits); - if (cacheEntry.ExpiresAt < SystemClock.UtcNow) - return Task.FromResult(false); + public Task GetExpirationAsync(string key) + { + if (String.IsNullOrEmpty(key)) + throw new ArgumentNullException(nameof(key), "Key cannot be null or empty"); - return Task.FromResult(true); + if (!_memory.TryGetValue(key, out var value) || value.ExpiresAt == DateTime.MaxValue) + { + Interlocked.Increment(ref _misses); + return Task.FromResult(null); } - public Task GetExpirationAsync(string key) - { - if (String.IsNullOrEmpty(key)) - throw new ArgumentNullException(nameof(key), "Key cannot be null or empty"); + Interlocked.Increment(ref _hits); + if (value.ExpiresAt >= SystemClock.UtcNow) + return Task.FromResult(value.ExpiresAt.Subtract(SystemClock.UtcNow)); - if (!_memory.TryGetValue(key, out var value) || value.ExpiresAt == DateTime.MaxValue) - { - Interlocked.Increment(ref _misses); - return Task.FromResult(null); - } + RemoveExpiredKey(key); - Interlocked.Increment(ref _hits); - if (value.ExpiresAt >= SystemClock.UtcNow) - return Task.FromResult(value.ExpiresAt.Subtract(SystemClock.UtcNow)); + return Task.FromResult(null); + } - RemoveExpiredKey(key); + public async Task SetExpirationAsync(string key, TimeSpan expiresIn) + { + if (String.IsNullOrEmpty(key)) + throw new ArgumentNullException(nameof(key), "Key cannot be null or empty"); - return Task.FromResult(null); + var expiresAt = SystemClock.UtcNow.SafeAdd(expiresIn); + if (expiresAt < SystemClock.UtcNow) + { + RemoveExpiredKey(key); + return; } - public async Task SetExpirationAsync(string key, TimeSpan expiresIn) + Interlocked.Increment(ref _writes); + if (_memory.TryGetValue(key, out var value)) { - if (String.IsNullOrEmpty(key)) - throw new ArgumentNullException(nameof(key), "Key cannot be null or empty"); - - var expiresAt = SystemClock.UtcNow.SafeAdd(expiresIn); - if (expiresAt < SystemClock.UtcNow) - { - RemoveExpiredKey(key); - return; - } - - Interlocked.Increment(ref _writes); - if (_memory.TryGetValue(key, out var value)) - { - value.ExpiresAt = expiresAt; - await StartMaintenanceAsync().AnyContext(); - } + value.ExpiresAt = expiresAt; + await StartMaintenanceAsync().AnyContext(); } + } - private DateTimeOffset _lastMaintenance; + private DateTimeOffset _lastMaintenance; - private async Task StartMaintenanceAsync(bool compactImmediately = false) - { - var now = SystemClock.UtcNow; - if (compactImmediately) - await CompactAsync().AnyContext(); + private async Task StartMaintenanceAsync(bool compactImmediately = false) + { + var now = SystemClock.UtcNow; + if (compactImmediately) + await CompactAsync().AnyContext(); - if (TimeSpan.FromMilliseconds(100) < now - _lastMaintenance) - { - _lastMaintenance = now; - _ = Task.Run(DoMaintenanceAsync); - } + if (TimeSpan.FromMilliseconds(100) < now - _lastMaintenance) + { + _lastMaintenance = now; + _ = Task.Run(DoMaintenanceAsync); } + } - private Task CompactAsync() + private Task CompactAsync() + { + if (!_maxItems.HasValue || _memory.Count <= _maxItems) + return Task.CompletedTask; + + string expiredKey = null; + lock (_lock) { - if (!_maxItems.HasValue || _memory.Count <= _maxItems) + if (_memory.Count <= _maxItems) return Task.CompletedTask; - string expiredKey = null; - lock (_lock) + (string Key, long LastAccessTicks, long InstanceNumber) oldest = (null, Int64.MaxValue, 0); + foreach (var kvp in _memory) { - if (_memory.Count <= _maxItems) - return Task.CompletedTask; - - (string Key, long LastAccessTicks, long InstanceNumber) oldest = (null, Int64.MaxValue, 0); - foreach (var kvp in _memory) - { - if (kvp.Value.LastAccessTicks < oldest.LastAccessTicks - || (kvp.Value.LastAccessTicks == oldest.LastAccessTicks && kvp.Value.InstanceNumber < oldest.InstanceNumber)) - oldest = (kvp.Key, kvp.Value.LastAccessTicks, kvp.Value.InstanceNumber); - } - - _logger.LogDebug("Removing cache entry {Key} due to cache exceeding max item count limit.", oldest); - _memory.TryRemove(oldest.Key, out var cacheEntry); - if (cacheEntry != null && cacheEntry.ExpiresAt < SystemClock.UtcNow) - expiredKey = oldest.Key; + if (kvp.Value.LastAccessTicks < oldest.LastAccessTicks + || (kvp.Value.LastAccessTicks == oldest.LastAccessTicks && kvp.Value.InstanceNumber < oldest.InstanceNumber)) + oldest = (kvp.Key, kvp.Value.LastAccessTicks, kvp.Value.InstanceNumber); } - if (expiredKey != null) - OnItemExpired(expiredKey); - - return Task.CompletedTask; + _logger.LogDebug("Removing cache entry {Key} due to cache exceeding max item count limit.", oldest); + _memory.TryRemove(oldest.Key, out var cacheEntry); + if (cacheEntry != null && cacheEntry.ExpiresAt < SystemClock.UtcNow) + expiredKey = oldest.Key; } - private async Task DoMaintenanceAsync() - { - _logger.LogTrace("DoMaintenance"); + if (expiredKey != null) + OnItemExpired(expiredKey); - var utcNow = SystemClock.UtcNow.AddMilliseconds(50); + return Task.CompletedTask; + } - try - { - foreach (var kvp in _memory.ToArray()) - { - var expiresAt = kvp.Value.ExpiresAt; - if (expiresAt <= utcNow) - RemoveExpiredKey(kvp.Key); - } - } - catch (Exception ex) + private async Task DoMaintenanceAsync() + { + _logger.LogTrace("DoMaintenance"); + + var utcNow = SystemClock.UtcNow.AddMilliseconds(50); + + try + { + foreach (var kvp in _memory.ToArray()) { - _logger.LogError(ex, "Error trying to find expired cache items"); + var expiresAt = kvp.Value.ExpiresAt; + if (expiresAt <= utcNow) + RemoveExpiredKey(kvp.Key); } - - await CompactAsync().AnyContext(); } - - public void Dispose() + catch (Exception ex) { - _memory.Clear(); - ItemExpired?.Dispose(); + _logger.LogError(ex, "Error trying to find expired cache items"); } - private class CacheEntry - { - private object _cacheValue; - private static long _instanceCount; - private readonly bool _shouldClone; + await CompactAsync().AnyContext(); + } + + public void Dispose() + { + _memory.Clear(); + ItemExpired?.Dispose(); + } + + private class CacheEntry + { + private object _cacheValue; + private static long _instanceCount; + private readonly bool _shouldClone; #if DEBUG - private long _usageCount; + private long _usageCount; #endif - public CacheEntry(object value, DateTime expiresAt, bool shouldClone = true) - { - _shouldClone = shouldClone && TypeRequiresCloning(value?.GetType()); - Value = value; - ExpiresAt = expiresAt; - LastModifiedTicks = SystemClock.UtcNow.Ticks; - InstanceNumber = Interlocked.Increment(ref _instanceCount); - } + public CacheEntry(object value, DateTime expiresAt, bool shouldClone = true) + { + _shouldClone = shouldClone && TypeRequiresCloning(value?.GetType()); + Value = value; + ExpiresAt = expiresAt; + LastModifiedTicks = SystemClock.UtcNow.Ticks; + InstanceNumber = Interlocked.Increment(ref _instanceCount); + } - internal long InstanceNumber { get; private set; } - internal DateTime ExpiresAt { get; set; } - internal long LastAccessTicks { get; private set; } - internal long LastModifiedTicks { get; private set; } + internal long InstanceNumber { get; private set; } + internal DateTime ExpiresAt { get; set; } + internal long LastAccessTicks { get; private set; } + internal long LastModifiedTicks { get; private set; } #if DEBUG - internal long UsageCount => _usageCount; + internal long UsageCount => _usageCount; #endif - internal object Value + internal object Value + { + get { - get - { - LastAccessTicks = SystemClock.UtcNow.Ticks; + LastAccessTicks = SystemClock.UtcNow.Ticks; #if DEBUG - Interlocked.Increment(ref _usageCount); + Interlocked.Increment(ref _usageCount); #endif - return _shouldClone ? _cacheValue.DeepClone() : _cacheValue; - } - set - { - _cacheValue = _shouldClone ? value.DeepClone() : value; - LastAccessTicks = SystemClock.UtcNow.Ticks; - LastModifiedTicks = SystemClock.UtcNow.Ticks; - } + return _shouldClone ? _cacheValue.DeepClone() : _cacheValue; } - - public T GetValue() + set { - object val = Value; - var t = typeof(T); + _cacheValue = _shouldClone ? value.DeepClone() : value; + LastAccessTicks = SystemClock.UtcNow.Ticks; + LastModifiedTicks = SystemClock.UtcNow.Ticks; + } + } - if (t == TypeHelper.BoolType || t == TypeHelper.StringType || t == TypeHelper.CharType || t == TypeHelper.DateTimeType || t == TypeHelper.ObjectType || t.IsNumeric()) - return (T)Convert.ChangeType(val, t); + public T GetValue() + { + object val = Value; + var t = typeof(T); - if (t == TypeHelper.NullableBoolType || t == TypeHelper.NullableCharType || t == TypeHelper.NullableDateTimeType || t.IsNullableNumeric()) - return val == null ? default : (T)Convert.ChangeType(val, Nullable.GetUnderlyingType(t)); + if (t == TypeHelper.BoolType || t == TypeHelper.StringType || t == TypeHelper.CharType || t == TypeHelper.DateTimeType || t == TypeHelper.ObjectType || t.IsNumeric()) + return (T)Convert.ChangeType(val, t); - return (T)val; - } + if (t == TypeHelper.NullableBoolType || t == TypeHelper.NullableCharType || t == TypeHelper.NullableDateTimeType || t.IsNullableNumeric()) + return val == null ? default : (T)Convert.ChangeType(val, Nullable.GetUnderlyingType(t)); - private bool TypeRequiresCloning(Type t) - { - if (t == null) - return true; - - if (t == TypeHelper.BoolType || - t == TypeHelper.NullableBoolType || - t == TypeHelper.StringType || - t == TypeHelper.CharType || - t == TypeHelper.NullableCharType || - t.IsNumeric() || - t.IsNullableNumeric()) - return false; + return (T)val; + } - return !t.GetTypeInfo().IsValueType; - } + private bool TypeRequiresCloning(Type t) + { + if (t == null) + return true; + + if (t == TypeHelper.BoolType || + t == TypeHelper.NullableBoolType || + t == TypeHelper.StringType || + t == TypeHelper.CharType || + t == TypeHelper.NullableCharType || + t.IsNumeric() || + t.IsNullableNumeric()) + return false; + + return !t.GetTypeInfo().IsValueType; } } +} - public class ItemExpiredEventArgs : EventArgs - { - public InMemoryCacheClient Client { get; set; } - public string Key { get; set; } - public bool SendNotification { get; set; } - } +public class ItemExpiredEventArgs : EventArgs +{ + public InMemoryCacheClient Client { get; set; } + public string Key { get; set; } + public bool SendNotification { get; set; } } diff --git a/src/Foundatio/Caching/InMemoryCacheClientOptions.cs b/src/Foundatio/Caching/InMemoryCacheClientOptions.cs index e6c7b3993..c608ad292 100644 --- a/src/Foundatio/Caching/InMemoryCacheClientOptions.cs +++ b/src/Foundatio/Caching/InMemoryCacheClientOptions.cs @@ -1,41 +1,40 @@ -namespace Foundatio.Caching +namespace Foundatio.Caching; + +public class InMemoryCacheClientOptions : SharedOptions { - public class InMemoryCacheClientOptions : SharedOptions - { - /// - /// The maximum number of items to store in the cache - /// - public int? MaxItems { get; set; } = 1000; + /// + /// The maximum number of items to store in the cache + /// + public int? MaxItems { get; set; } = 1000; - /// - /// Whether or not values should be cloned during get and set to make sure that any cache entry changes are isolated - /// - public bool CloneValues { get; set; } = false; + /// + /// Whether or not values should be cloned during get and set to make sure that any cache entry changes are isolated + /// + public bool CloneValues { get; set; } = false; - /// - /// Whether or not an error when deserializing a cache value should result in an exception being thrown or if it should just return an empty cache value - /// - public bool ShouldThrowOnSerializationError { get; set; } = true; - } + /// + /// Whether or not an error when deserializing a cache value should result in an exception being thrown or if it should just return an empty cache value + /// + public bool ShouldThrowOnSerializationError { get; set; } = true; +} - public class InMemoryCacheClientOptionsBuilder : SharedOptionsBuilder +public class InMemoryCacheClientOptionsBuilder : SharedOptionsBuilder +{ + public InMemoryCacheClientOptionsBuilder MaxItems(int? maxItems) { - public InMemoryCacheClientOptionsBuilder MaxItems(int? maxItems) - { - Target.MaxItems = maxItems; - return this; - } + Target.MaxItems = maxItems; + return this; + } - public InMemoryCacheClientOptionsBuilder CloneValues(bool cloneValues) - { - Target.CloneValues = cloneValues; - return this; - } + public InMemoryCacheClientOptionsBuilder CloneValues(bool cloneValues) + { + Target.CloneValues = cloneValues; + return this; + } - public InMemoryCacheClientOptionsBuilder ShouldThrowOnSerializationError(bool shouldThrow) - { - Target.ShouldThrowOnSerializationError = shouldThrow; - return this; - } + public InMemoryCacheClientOptionsBuilder ShouldThrowOnSerializationError(bool shouldThrow) + { + Target.ShouldThrowOnSerializationError = shouldThrow; + return this; } } diff --git a/src/Foundatio/Caching/NullCacheClient.cs b/src/Foundatio/Caching/NullCacheClient.cs index 340dd5b20..1074002e6 100644 --- a/src/Foundatio/Caching/NullCacheClient.cs +++ b/src/Foundatio/Caching/NullCacheClient.cs @@ -4,191 +4,190 @@ using System.Threading; using System.Threading.Tasks; -namespace Foundatio.Caching -{ - public class NullCacheClient : ICacheClient - { - public static readonly NullCacheClient Instance = new(); +namespace Foundatio.Caching; - private long _writes; - private long _reads; +public class NullCacheClient : ICacheClient +{ + public static readonly NullCacheClient Instance = new(); - public long Calls => _writes + _reads; - public long Writes => _writes; - public long Reads => _reads; + private long _writes; + private long _reads; - public override string ToString() - { - return $"Calls: {Calls} Reads: {Reads} Writes: {Writes}"; - } + public long Calls => _writes + _reads; + public long Writes => _writes; + public long Reads => _reads; - public void ResetStats() - { - _writes = 0; - _reads = 0; - } + public override string ToString() + { + return $"Calls: {Calls} Reads: {Reads} Writes: {Writes}"; + } - public Task RemoveAsync(string key) - { - Interlocked.Increment(ref _writes); + public void ResetStats() + { + _writes = 0; + _reads = 0; + } - return Task.FromResult(false); - } + public Task RemoveAsync(string key) + { + Interlocked.Increment(ref _writes); - public Task RemoveIfEqualAsync(string key, T expected) - { - Interlocked.Increment(ref _writes); + return Task.FromResult(false); + } - return Task.FromResult(false); - } + public Task RemoveIfEqualAsync(string key, T expected) + { + Interlocked.Increment(ref _writes); - public Task RemoveAllAsync(IEnumerable keys = null) - { - Interlocked.Increment(ref _writes); + return Task.FromResult(false); + } - return Task.FromResult(0); - } + public Task RemoveAllAsync(IEnumerable keys = null) + { + Interlocked.Increment(ref _writes); - public Task RemoveByPrefixAsync(string prefix) - { - Interlocked.Increment(ref _writes); + return Task.FromResult(0); + } - return Task.FromResult(0); - } + public Task RemoveByPrefixAsync(string prefix) + { + Interlocked.Increment(ref _writes); - public Task> GetAsync(string key) - { - Interlocked.Increment(ref _reads); + return Task.FromResult(0); + } - return Task.FromResult(CacheValue.NoValue); - } + public Task> GetAsync(string key) + { + Interlocked.Increment(ref _reads); - public Task>> GetAllAsync(IEnumerable keys) - { - Interlocked.Increment(ref _reads); + return Task.FromResult(CacheValue.NoValue); + } - return Task.FromResult>>(keys.ToDictionary(k => k, k => CacheValue.NoValue)); - } + public Task>> GetAllAsync(IEnumerable keys) + { + Interlocked.Increment(ref _reads); - public Task AddAsync(string key, T value, TimeSpan? expiresIn = null) - { - Interlocked.Increment(ref _writes); + return Task.FromResult>>(keys.ToDictionary(k => k, k => CacheValue.NoValue)); + } - return Task.FromResult(true); - } + public Task AddAsync(string key, T value, TimeSpan? expiresIn = null) + { + Interlocked.Increment(ref _writes); - public Task SetAsync(string key, T value, TimeSpan? expiresIn = null) - { - Interlocked.Increment(ref _writes); + return Task.FromResult(true); + } - return Task.FromResult(true); - } + public Task SetAsync(string key, T value, TimeSpan? expiresIn = null) + { + Interlocked.Increment(ref _writes); - public Task SetAllAsync(IDictionary values, TimeSpan? expiresIn = null) - { - Interlocked.Increment(ref _writes); + return Task.FromResult(true); + } - return Task.FromResult(0); - } + public Task SetAllAsync(IDictionary values, TimeSpan? expiresIn = null) + { + Interlocked.Increment(ref _writes); - public Task ReplaceAsync(string key, T value, TimeSpan? expiresIn = null) - { - Interlocked.Increment(ref _writes); + return Task.FromResult(0); + } - return Task.FromResult(true); - } + public Task ReplaceAsync(string key, T value, TimeSpan? expiresIn = null) + { + Interlocked.Increment(ref _writes); - public Task ReplaceIfEqualAsync(string key, T value, T expected, TimeSpan? expiresIn = null) - { - Interlocked.Increment(ref _writes); + return Task.FromResult(true); + } - return Task.FromResult(true); - } + public Task ReplaceIfEqualAsync(string key, T value, T expected, TimeSpan? expiresIn = null) + { + Interlocked.Increment(ref _writes); - public Task IncrementAsync(string key, double amount, TimeSpan? expiresIn = null) - { - Interlocked.Increment(ref _writes); + return Task.FromResult(true); + } - return Task.FromResult(amount); - } + public Task IncrementAsync(string key, double amount, TimeSpan? expiresIn = null) + { + Interlocked.Increment(ref _writes); - public Task IncrementAsync(string key, long amount, TimeSpan? expiresIn = null) - { - Interlocked.Increment(ref _writes); + return Task.FromResult(amount); + } - return Task.FromResult(amount); - } + public Task IncrementAsync(string key, long amount, TimeSpan? expiresIn = null) + { + Interlocked.Increment(ref _writes); - public Task ExistsAsync(string key) - { - Interlocked.Increment(ref _reads); + return Task.FromResult(amount); + } - return Task.FromResult(false); - } + public Task ExistsAsync(string key) + { + Interlocked.Increment(ref _reads); - public Task GetExpirationAsync(string key) - { - Interlocked.Increment(ref _reads); + return Task.FromResult(false); + } - return Task.FromResult(null); - } + public Task GetExpirationAsync(string key) + { + Interlocked.Increment(ref _reads); - public Task SetExpirationAsync(string key, TimeSpan expiresIn) - { - Interlocked.Increment(ref _writes); + return Task.FromResult(null); + } - return Task.FromResult(0); - } + public Task SetExpirationAsync(string key, TimeSpan expiresIn) + { + Interlocked.Increment(ref _writes); - public Task SetIfHigherAsync(string key, double value, TimeSpan? expiresIn = null) - { - Interlocked.Increment(ref _writes); + return Task.FromResult(0); + } - return Task.FromResult(value); - } + public Task SetIfHigherAsync(string key, double value, TimeSpan? expiresIn = null) + { + Interlocked.Increment(ref _writes); - public Task SetIfHigherAsync(string key, long value, TimeSpan? expiresIn = null) - { - Interlocked.Increment(ref _writes); + return Task.FromResult(value); + } - return Task.FromResult(value); - } + public Task SetIfHigherAsync(string key, long value, TimeSpan? expiresIn = null) + { + Interlocked.Increment(ref _writes); - public Task SetIfLowerAsync(string key, double value, TimeSpan? expiresIn = null) - { - Interlocked.Increment(ref _writes); + return Task.FromResult(value); + } - return Task.FromResult(value); - } + public Task SetIfLowerAsync(string key, double value, TimeSpan? expiresIn = null) + { + Interlocked.Increment(ref _writes); - public Task SetIfLowerAsync(string key, long value, TimeSpan? expiresIn = null) - { - Interlocked.Increment(ref _writes); + return Task.FromResult(value); + } - return Task.FromResult(value); - } + public Task SetIfLowerAsync(string key, long value, TimeSpan? expiresIn = null) + { + Interlocked.Increment(ref _writes); - public Task ListAddAsync(string key, IEnumerable values, TimeSpan? expiresIn = null) - { - Interlocked.Increment(ref _writes); + return Task.FromResult(value); + } - return Task.FromResult(default(long)); - } + public Task ListAddAsync(string key, IEnumerable values, TimeSpan? expiresIn = null) + { + Interlocked.Increment(ref _writes); - public Task ListRemoveAsync(string key, IEnumerable values, TimeSpan? expiresIn = null) - { - Interlocked.Increment(ref _writes); + return Task.FromResult(default(long)); + } - return Task.FromResult(default(long)); - } + public Task ListRemoveAsync(string key, IEnumerable values, TimeSpan? expiresIn = null) + { + Interlocked.Increment(ref _writes); - public Task>> GetListAsync(string key, int? page = null, int pageSize = 100) - { - Interlocked.Increment(ref _reads); + return Task.FromResult(default(long)); + } - return Task.FromResult(CacheValue>.NoValue); - } + public Task>> GetListAsync(string key, int? page = null, int pageSize = 100) + { + Interlocked.Increment(ref _reads); - public void Dispose() { } + return Task.FromResult(CacheValue>.NoValue); } + + public void Dispose() { } } diff --git a/src/Foundatio/Caching/ScopedCacheClient.cs b/src/Foundatio/Caching/ScopedCacheClient.cs index fac234bce..880f4084a 100644 --- a/src/Foundatio/Caching/ScopedCacheClient.cs +++ b/src/Foundatio/Caching/ScopedCacheClient.cs @@ -4,182 +4,181 @@ using System.Threading.Tasks; using Foundatio.Utility; -namespace Foundatio.Caching +namespace Foundatio.Caching; + +public class ScopedHybridCacheClient : ScopedCacheClient, IHybridCacheClient { - public class ScopedHybridCacheClient : ScopedCacheClient, IHybridCacheClient - { - public ScopedHybridCacheClient(IHybridCacheClient client, string scope = null) : base(client, scope) { } - } + public ScopedHybridCacheClient(IHybridCacheClient client, string scope = null) : base(client, scope) { } +} - public class ScopedCacheClient : ICacheClient +public class ScopedCacheClient : ICacheClient +{ + private string _keyPrefix; + private bool _isLocked; + private readonly object _lock = new(); + + public ScopedCacheClient(ICacheClient client, string scope = null) { - private string _keyPrefix; - private bool _isLocked; - private readonly object _lock = new(); + UnscopedCache = client ?? new NullCacheClient(); + _isLocked = scope != null; + Scope = !String.IsNullOrWhiteSpace(scope) ? scope.Trim() : null; - public ScopedCacheClient(ICacheClient client, string scope = null) - { - UnscopedCache = client ?? new NullCacheClient(); - _isLocked = scope != null; - Scope = !String.IsNullOrWhiteSpace(scope) ? scope.Trim() : null; + _keyPrefix = Scope != null ? String.Concat(Scope, ":") : String.Empty; + } - _keyPrefix = Scope != null ? String.Concat(Scope, ":") : String.Empty; - } + public ICacheClient UnscopedCache { get; private set; } - public ICacheClient UnscopedCache { get; private set; } + public string Scope { get; private set; } - public string Scope { get; private set; } + public void SetScope(string scope) + { + if (_isLocked) + throw new InvalidOperationException("Scope can't be changed after it has been set"); - public void SetScope(string scope) + lock (_lock) { if (_isLocked) throw new InvalidOperationException("Scope can't be changed after it has been set"); - lock (_lock) - { - if (_isLocked) - throw new InvalidOperationException("Scope can't be changed after it has been set"); - - _isLocked = true; - Scope = !String.IsNullOrWhiteSpace(scope) ? scope.Trim() : null; - _keyPrefix = Scope != null ? String.Concat(Scope, ":") : String.Empty; - } - } - - protected string GetUnscopedCacheKey(string key) - { - return String.Concat(_keyPrefix, key); + _isLocked = true; + Scope = !String.IsNullOrWhiteSpace(scope) ? scope.Trim() : null; + _keyPrefix = Scope != null ? String.Concat(Scope, ":") : String.Empty; } + } - protected IEnumerable GetUnscopedCacheKeys(IEnumerable keys) - { - return keys?.Select(GetUnscopedCacheKey); - } + protected string GetUnscopedCacheKey(string key) + { + return String.Concat(_keyPrefix, key); + } - protected string GetScopedCacheKey(string unscopedKey) - { - return unscopedKey?.Substring(_keyPrefix.Length); - } + protected IEnumerable GetUnscopedCacheKeys(IEnumerable keys) + { + return keys?.Select(GetUnscopedCacheKey); + } - public Task RemoveAsync(string key) - { - return UnscopedCache.RemoveAsync(GetUnscopedCacheKey(key)); - } + protected string GetScopedCacheKey(string unscopedKey) + { + return unscopedKey?.Substring(_keyPrefix.Length); + } - public Task RemoveIfEqualAsync(string key, T expected) - { - return UnscopedCache.RemoveIfEqualAsync(GetUnscopedCacheKey(key), expected); - } + public Task RemoveAsync(string key) + { + return UnscopedCache.RemoveAsync(GetUnscopedCacheKey(key)); + } - public Task RemoveAllAsync(IEnumerable keys = null) - { - if (keys == null) - return RemoveByPrefixAsync(String.Empty); + public Task RemoveIfEqualAsync(string key, T expected) + { + return UnscopedCache.RemoveIfEqualAsync(GetUnscopedCacheKey(key), expected); + } - return UnscopedCache.RemoveAllAsync(GetUnscopedCacheKeys(keys)); - } + public Task RemoveAllAsync(IEnumerable keys = null) + { + if (keys == null) + return RemoveByPrefixAsync(String.Empty); - public Task RemoveByPrefixAsync(string prefix) - { - return UnscopedCache.RemoveByPrefixAsync(GetUnscopedCacheKey(prefix)); - } + return UnscopedCache.RemoveAllAsync(GetUnscopedCacheKeys(keys)); + } - public Task> GetAsync(string key) - { - return UnscopedCache.GetAsync(GetUnscopedCacheKey(key)); - } + public Task RemoveByPrefixAsync(string prefix) + { + return UnscopedCache.RemoveByPrefixAsync(GetUnscopedCacheKey(prefix)); + } - public async Task>> GetAllAsync(IEnumerable keys) - { - var scopedDictionary = await UnscopedCache.GetAllAsync(GetUnscopedCacheKeys(keys)).AnyContext(); - return scopedDictionary.ToDictionary(kvp => GetScopedCacheKey(kvp.Key), kvp => kvp.Value); - } + public Task> GetAsync(string key) + { + return UnscopedCache.GetAsync(GetUnscopedCacheKey(key)); + } - public Task AddAsync(string key, T value, TimeSpan? expiresIn = null) - { - return UnscopedCache.AddAsync(GetUnscopedCacheKey(key), value, expiresIn); - } + public async Task>> GetAllAsync(IEnumerable keys) + { + var scopedDictionary = await UnscopedCache.GetAllAsync(GetUnscopedCacheKeys(keys)).AnyContext(); + return scopedDictionary.ToDictionary(kvp => GetScopedCacheKey(kvp.Key), kvp => kvp.Value); + } - public Task SetAsync(string key, T value, TimeSpan? expiresIn = null) - { - return UnscopedCache.SetAsync(GetUnscopedCacheKey(key), value, expiresIn); - } + public Task AddAsync(string key, T value, TimeSpan? expiresIn = null) + { + return UnscopedCache.AddAsync(GetUnscopedCacheKey(key), value, expiresIn); + } - public Task SetAllAsync(IDictionary values, TimeSpan? expiresIn = null) - { - return UnscopedCache.SetAllAsync(values?.ToDictionary(kvp => GetUnscopedCacheKey(kvp.Key), kvp => kvp.Value), expiresIn); - } + public Task SetAsync(string key, T value, TimeSpan? expiresIn = null) + { + return UnscopedCache.SetAsync(GetUnscopedCacheKey(key), value, expiresIn); + } - public Task ReplaceAsync(string key, T value, TimeSpan? expiresIn = null) - { - return UnscopedCache.ReplaceAsync(GetUnscopedCacheKey(key), value, expiresIn); - } + public Task SetAllAsync(IDictionary values, TimeSpan? expiresIn = null) + { + return UnscopedCache.SetAllAsync(values?.ToDictionary(kvp => GetUnscopedCacheKey(kvp.Key), kvp => kvp.Value), expiresIn); + } - public Task ReplaceIfEqualAsync(string key, T value, T expected, TimeSpan? expiresIn = null) - { - return UnscopedCache.ReplaceIfEqualAsync(GetUnscopedCacheKey(key), value, expected, expiresIn); - } + public Task ReplaceAsync(string key, T value, TimeSpan? expiresIn = null) + { + return UnscopedCache.ReplaceAsync(GetUnscopedCacheKey(key), value, expiresIn); + } - public Task IncrementAsync(string key, double amount, TimeSpan? expiresIn = null) - { - return UnscopedCache.IncrementAsync(GetUnscopedCacheKey(key), amount, expiresIn); - } + public Task ReplaceIfEqualAsync(string key, T value, T expected, TimeSpan? expiresIn = null) + { + return UnscopedCache.ReplaceIfEqualAsync(GetUnscopedCacheKey(key), value, expected, expiresIn); + } - public Task IncrementAsync(string key, long amount, TimeSpan? expiresIn = null) - { - return UnscopedCache.IncrementAsync(GetUnscopedCacheKey(key), amount, expiresIn); - } + public Task IncrementAsync(string key, double amount, TimeSpan? expiresIn = null) + { + return UnscopedCache.IncrementAsync(GetUnscopedCacheKey(key), amount, expiresIn); + } - public Task ExistsAsync(string key) - { - return UnscopedCache.ExistsAsync(GetUnscopedCacheKey(key)); - } + public Task IncrementAsync(string key, long amount, TimeSpan? expiresIn = null) + { + return UnscopedCache.IncrementAsync(GetUnscopedCacheKey(key), amount, expiresIn); + } - public Task GetExpirationAsync(string key) - { - return UnscopedCache.GetExpirationAsync(GetUnscopedCacheKey(key)); - } + public Task ExistsAsync(string key) + { + return UnscopedCache.ExistsAsync(GetUnscopedCacheKey(key)); + } - public Task SetExpirationAsync(string key, TimeSpan expiresIn) - { - return UnscopedCache.SetExpirationAsync(GetUnscopedCacheKey(key), expiresIn); - } + public Task GetExpirationAsync(string key) + { + return UnscopedCache.GetExpirationAsync(GetUnscopedCacheKey(key)); + } - public Task SetIfHigherAsync(string key, double value, TimeSpan? expiresIn = null) - { - return UnscopedCache.SetIfHigherAsync(GetUnscopedCacheKey(key), value, expiresIn); - } + public Task SetExpirationAsync(string key, TimeSpan expiresIn) + { + return UnscopedCache.SetExpirationAsync(GetUnscopedCacheKey(key), expiresIn); + } - public Task SetIfHigherAsync(string key, long value, TimeSpan? expiresIn = null) - { - return UnscopedCache.SetIfHigherAsync(GetUnscopedCacheKey(key), value, expiresIn); - } + public Task SetIfHigherAsync(string key, double value, TimeSpan? expiresIn = null) + { + return UnscopedCache.SetIfHigherAsync(GetUnscopedCacheKey(key), value, expiresIn); + } - public Task SetIfLowerAsync(string key, double value, TimeSpan? expiresIn = null) - { - return UnscopedCache.SetIfLowerAsync(GetUnscopedCacheKey(key), value, expiresIn); - } + public Task SetIfHigherAsync(string key, long value, TimeSpan? expiresIn = null) + { + return UnscopedCache.SetIfHigherAsync(GetUnscopedCacheKey(key), value, expiresIn); + } - public Task SetIfLowerAsync(string key, long value, TimeSpan? expiresIn = null) - { - return UnscopedCache.SetIfLowerAsync(GetUnscopedCacheKey(key), value, expiresIn); - } + public Task SetIfLowerAsync(string key, double value, TimeSpan? expiresIn = null) + { + return UnscopedCache.SetIfLowerAsync(GetUnscopedCacheKey(key), value, expiresIn); + } - public Task ListAddAsync(string key, IEnumerable values, TimeSpan? expiresIn = null) - { - return UnscopedCache.ListAddAsync(GetUnscopedCacheKey(key), values, expiresIn); - } + public Task SetIfLowerAsync(string key, long value, TimeSpan? expiresIn = null) + { + return UnscopedCache.SetIfLowerAsync(GetUnscopedCacheKey(key), value, expiresIn); + } - public Task ListRemoveAsync(string key, IEnumerable values, TimeSpan? expiresIn = null) - { - return UnscopedCache.ListRemoveAsync(GetUnscopedCacheKey(key), values, expiresIn); - } + public Task ListAddAsync(string key, IEnumerable values, TimeSpan? expiresIn = null) + { + return UnscopedCache.ListAddAsync(GetUnscopedCacheKey(key), values, expiresIn); + } - public Task>> GetListAsync(string key, int? page = null, int pageSize = 100) - { - return UnscopedCache.GetListAsync(GetUnscopedCacheKey(key), page, pageSize); - } + public Task ListRemoveAsync(string key, IEnumerable values, TimeSpan? expiresIn = null) + { + return UnscopedCache.ListRemoveAsync(GetUnscopedCacheKey(key), values, expiresIn); + } - public void Dispose() { } + public Task>> GetListAsync(string key, int? page = null, int pageSize = 100) + { + return UnscopedCache.GetListAsync(GetUnscopedCacheKey(key), page, pageSize); } + + public void Dispose() { } } diff --git a/src/Foundatio/DeepCloner/DeepClonerExtensions.cs b/src/Foundatio/DeepCloner/DeepClonerExtensions.cs index 06237fbca..5fa771782 100644 --- a/src/Foundatio/DeepCloner/DeepClonerExtensions.cs +++ b/src/Foundatio/DeepCloner/DeepClonerExtensions.cs @@ -3,75 +3,74 @@ using Foundatio.Force.DeepCloner.Helpers; -namespace Foundatio.Force.DeepCloner +namespace Foundatio.Force.DeepCloner; + +/// +/// Extensions for object cloning +/// +internal static class DeepClonerExtensions { /// - /// Extensions for object cloning + /// Performs deep (full) copy of object and related graph /// - internal static class DeepClonerExtensions + public static T DeepClone(this T obj) { - /// - /// Performs deep (full) copy of object and related graph - /// - public static T DeepClone(this T obj) - { - return DeepClonerGenerator.CloneObject(obj); - } + return DeepClonerGenerator.CloneObject(obj); + } - /// - /// Performs deep (full) copy of object and related graph to existing object - /// - /// existing filled object - /// Method is valid only for classes, classes should be descendants in reality, not in declaration - public static TTo DeepCloneTo(this TFrom objFrom, TTo objTo) where TTo : class, TFrom - { - return (TTo)DeepClonerGenerator.CloneObjectTo(objFrom, objTo, true); - } + /// + /// Performs deep (full) copy of object and related graph to existing object + /// + /// existing filled object + /// Method is valid only for classes, classes should be descendants in reality, not in declaration + public static TTo DeepCloneTo(this TFrom objFrom, TTo objTo) where TTo : class, TFrom + { + return (TTo)DeepClonerGenerator.CloneObjectTo(objFrom, objTo, true); + } - /// - /// Performs shallow copy of object to existing object - /// - /// existing filled object - /// Method is valid only for classes, classes should be descendants in reality, not in declaration - public static TTo ShallowCloneTo(this TFrom objFrom, TTo objTo) where TTo : class, TFrom + /// + /// Performs shallow copy of object to existing object + /// + /// existing filled object + /// Method is valid only for classes, classes should be descendants in reality, not in declaration + public static TTo ShallowCloneTo(this TFrom objFrom, TTo objTo) where TTo : class, TFrom + { + return (TTo)DeepClonerGenerator.CloneObjectTo(objFrom, objTo, false); + } + + /// + /// Performs shallow (only new object returned, without cloning of dependencies) copy of object + /// + public static T ShallowClone(this T obj) + { + return ShallowClonerGenerator.CloneObject(obj); + } + + static DeepClonerExtensions() + { + if (!PermissionCheck()) { - return (TTo)DeepClonerGenerator.CloneObjectTo(objFrom, objTo, false); + throw new SecurityException("DeepCloner should have enough permissions to run. Grant FullTrust or Reflection permission"); } + } - /// - /// Performs shallow (only new object returned, without cloning of dependencies) copy of object - /// - public static T ShallowClone(this T obj) + private static bool PermissionCheck() + { + // best way to check required permission: execute something and receive exception + // .net security policy is weird for normal usage + try { - return ShallowClonerGenerator.CloneObject(obj); + new object().ShallowClone(); } - - static DeepClonerExtensions() + catch (VerificationException) { - if (!PermissionCheck()) - { - throw new SecurityException("DeepCloner should have enough permissions to run. Grant FullTrust or Reflection permission"); - } + return false; } - - private static bool PermissionCheck() + catch (MemberAccessException) { - // best way to check required permission: execute something and receive exception - // .net security policy is weird for normal usage - try - { - new object().ShallowClone(); - } - catch (VerificationException) - { - return false; - } - catch (MemberAccessException) - { - return false; - } - - return true; + return false; } + + return true; } } diff --git a/src/Foundatio/DeepCloner/Helpers/ClonerToExprGenerator.cs b/src/Foundatio/DeepCloner/Helpers/ClonerToExprGenerator.cs index c46572ec2..00f327375 100644 --- a/src/Foundatio/DeepCloner/Helpers/ClonerToExprGenerator.cs +++ b/src/Foundatio/DeepCloner/Helpers/ClonerToExprGenerator.cs @@ -6,289 +6,288 @@ using System.Linq.Expressions; using System.Reflection; -namespace Foundatio.Force.DeepCloner.Helpers +namespace Foundatio.Force.DeepCloner.Helpers; + +internal static class ClonerToExprGenerator { - internal static class ClonerToExprGenerator + internal static object GenerateClonerInternal(Type realType, bool isDeepClone) + { + if (realType.IsValueType()) + throw new InvalidOperationException("Operation is valid only for reference types"); + return GenerateProcessMethod(realType, isDeepClone); + } + + private static object GenerateProcessMethod(Type type, bool isDeepClone) { - internal static object GenerateClonerInternal(Type realType, bool isDeepClone) + if (type.IsArray) { - if (realType.IsValueType()) - throw new InvalidOperationException("Operation is valid only for reference types"); - return GenerateProcessMethod(realType, isDeepClone); + return GenerateProcessArrayMethod(type, isDeepClone); } - private static object GenerateProcessMethod(Type type, bool isDeepClone) - { - if (type.IsArray) - { - return GenerateProcessArrayMethod(type, isDeepClone); - } + var methodType = typeof(object); - var methodType = typeof(object); + var expressionList = new List(); - var expressionList = new List(); + ParameterExpression from = Expression.Parameter(methodType); + var fromLocal = from; + var to = Expression.Parameter(methodType); + var toLocal = to; + var state = Expression.Parameter(typeof(DeepCloneState)); - ParameterExpression from = Expression.Parameter(methodType); - var fromLocal = from; - var to = Expression.Parameter(methodType); - var toLocal = to; - var state = Expression.Parameter(typeof(DeepCloneState)); + // if (!type.IsValueType()) + { + fromLocal = Expression.Variable(type); + toLocal = Expression.Variable(type); + // fromLocal = (T)from + expressionList.Add(Expression.Assign(fromLocal, Expression.Convert(from, type))); + expressionList.Add(Expression.Assign(toLocal, Expression.Convert(to, type))); - // if (!type.IsValueType()) + if (isDeepClone) { - fromLocal = Expression.Variable(type); - toLocal = Expression.Variable(type); - // fromLocal = (T)from - expressionList.Add(Expression.Assign(fromLocal, Expression.Convert(from, type))); - expressionList.Add(Expression.Assign(toLocal, Expression.Convert(to, type))); - - if (isDeepClone) - { - // added from -> to binding to ensure reference loop handling - // structs cannot loop here - // state.AddKnownRef(from, to) - expressionList.Add(Expression.Call(state, typeof(DeepCloneState).GetMethod("AddKnownRef"), from, to)); - } + // added from -> to binding to ensure reference loop handling + // structs cannot loop here + // state.AddKnownRef(from, to) + expressionList.Add(Expression.Call(state, typeof(DeepCloneState).GetMethod("AddKnownRef"), from, to)); } + } - List fi = new List(); - var tp = type; - do - { + List fi = new List(); + var tp = type; + do + { #if !NETCORE // don't do anything with this dark magic! if (tp == typeof(ContextBoundObject)) break; #else - if (tp.Name == "ContextBoundObject") break; + if (tp.Name == "ContextBoundObject") break; #endif - fi.AddRange(tp.GetDeclaredFields()); - tp = tp.BaseType(); - } - while (tp != null); + fi.AddRange(tp.GetDeclaredFields()); + tp = tp.BaseType(); + } + while (tp != null); - foreach (var fieldInfo in fi) + foreach (var fieldInfo in fi) + { + if (isDeepClone && !DeepClonerSafeTypes.CanReturnSameObject(fieldInfo.FieldType)) { - if (isDeepClone && !DeepClonerSafeTypes.CanReturnSameObject(fieldInfo.FieldType)) + var methodInfo = fieldInfo.FieldType.IsValueType() + ? typeof(DeepClonerGenerator).GetPrivateStaticMethod("CloneStructInternal") + .MakeGenericMethod(fieldInfo.FieldType) + : typeof(DeepClonerGenerator).GetPrivateStaticMethod("CloneClassInternal"); + + var get = Expression.Field(fromLocal, fieldInfo); + + // toLocal.Field = Clone...Internal(fromLocal.Field) + var call = (Expression)Expression.Call(methodInfo, get, state); + if (!fieldInfo.FieldType.IsValueType()) + call = Expression.Convert(call, fieldInfo.FieldType); + + // should handle specially + // todo: think about optimization, but it rare case + if (fieldInfo.IsInitOnly) { - var methodInfo = fieldInfo.FieldType.IsValueType() - ? typeof(DeepClonerGenerator).GetPrivateStaticMethod("CloneStructInternal") - .MakeGenericMethod(fieldInfo.FieldType) - : typeof(DeepClonerGenerator).GetPrivateStaticMethod("CloneClassInternal"); - - var get = Expression.Field(fromLocal, fieldInfo); - - // toLocal.Field = Clone...Internal(fromLocal.Field) - var call = (Expression)Expression.Call(methodInfo, get, state); - if (!fieldInfo.FieldType.IsValueType()) - call = Expression.Convert(call, fieldInfo.FieldType); - - // should handle specially - // todo: think about optimization, but it rare case - if (fieldInfo.IsInitOnly) - { - // var setMethod = fieldInfo.GetType().GetMethod("SetValue", new[] { typeof(object), typeof(object) }); - // expressionList.Add(Expression.Call(Expression.Constant(fieldInfo), setMethod, toLocal, call)); - var setMethod = typeof(DeepClonerExprGenerator).GetPrivateStaticMethod("ForceSetField"); - expressionList.Add(Expression.Call(setMethod, Expression.Constant(fieldInfo), - Expression.Convert(toLocal, typeof(object)), Expression.Convert(call, typeof(object)))); - } - else - { - expressionList.Add(Expression.Assign(Expression.Field(toLocal, fieldInfo), call)); - } + // var setMethod = fieldInfo.GetType().GetMethod("SetValue", new[] { typeof(object), typeof(object) }); + // expressionList.Add(Expression.Call(Expression.Constant(fieldInfo), setMethod, toLocal, call)); + var setMethod = typeof(DeepClonerExprGenerator).GetPrivateStaticMethod("ForceSetField"); + expressionList.Add(Expression.Call(setMethod, Expression.Constant(fieldInfo), + Expression.Convert(toLocal, typeof(object)), Expression.Convert(call, typeof(object)))); } else { - expressionList.Add(Expression.Assign(Expression.Field(toLocal, fieldInfo), Expression.Field(fromLocal, fieldInfo))); + expressionList.Add(Expression.Assign(Expression.Field(toLocal, fieldInfo), call)); } } + else + { + expressionList.Add(Expression.Assign(Expression.Field(toLocal, fieldInfo), Expression.Field(fromLocal, fieldInfo))); + } + } - expressionList.Add(Expression.Convert(toLocal, methodType)); + expressionList.Add(Expression.Convert(toLocal, methodType)); - var funcType = typeof(Func<,,,>).MakeGenericType(methodType, methodType, typeof(DeepCloneState), methodType); + var funcType = typeof(Func<,,,>).MakeGenericType(methodType, methodType, typeof(DeepCloneState), methodType); - var blockParams = new List(); - if (from != fromLocal) blockParams.Add(fromLocal); - if (to != toLocal) blockParams.Add(toLocal); + var blockParams = new List(); + if (from != fromLocal) blockParams.Add(fromLocal); + if (to != toLocal) blockParams.Add(toLocal); - return Expression.Lambda(funcType, Expression.Block(blockParams, expressionList), from, to, state).Compile(); - } + return Expression.Lambda(funcType, Expression.Block(blockParams, expressionList), from, to, state).Compile(); + } - private static object GenerateProcessArrayMethod(Type type, bool isDeep) - { - var elementType = type.GetElementType(); - var rank = type.GetArrayRank(); + private static object GenerateProcessArrayMethod(Type type, bool isDeep) + { + var elementType = type.GetElementType(); + var rank = type.GetArrayRank(); - ParameterExpression from = Expression.Parameter(typeof(object)); - ParameterExpression to = Expression.Parameter(typeof(object)); - var state = Expression.Parameter(typeof(DeepCloneState)); + ParameterExpression from = Expression.Parameter(typeof(object)); + ParameterExpression to = Expression.Parameter(typeof(object)); + var state = Expression.Parameter(typeof(DeepCloneState)); - var funcType = typeof(Func<,,,>).MakeGenericType(typeof(object), typeof(object), typeof(DeepCloneState), typeof(object)); + var funcType = typeof(Func<,,,>).MakeGenericType(typeof(object), typeof(object), typeof(DeepCloneState), typeof(object)); - if (rank == 1 && type == elementType.MakeArrayType()) + if (rank == 1 && type == elementType.MakeArrayType()) + { + if (!isDeep) { - if (!isDeep) - { - var callS = Expression.Call( - typeof(ClonerToExprGenerator).GetPrivateStaticMethod("ShallowClone1DimArraySafeInternal") - .MakeGenericMethod(elementType), Expression.Convert(from, type), Expression.Convert(to, type)); - return Expression.Lambda(funcType, callS, from, to, state).Compile(); - } - else - { - var methodName = "Clone1DimArrayClassInternal"; - if (DeepClonerSafeTypes.CanReturnSameObject(elementType)) methodName = "Clone1DimArraySafeInternal"; - else if (elementType.IsValueType()) methodName = "Clone1DimArrayStructInternal"; - var methodInfo = typeof(ClonerToExprGenerator).GetPrivateStaticMethod(methodName).MakeGenericMethod(elementType); - var callS = Expression.Call(methodInfo, Expression.Convert(from, type), Expression.Convert(to, type), state); - return Expression.Lambda(funcType, callS, from, to, state).Compile(); - } + var callS = Expression.Call( + typeof(ClonerToExprGenerator).GetPrivateStaticMethod("ShallowClone1DimArraySafeInternal") + .MakeGenericMethod(elementType), Expression.Convert(from, type), Expression.Convert(to, type)); + return Expression.Lambda(funcType, callS, from, to, state).Compile(); } else { - // multidim or not zero-based arrays - MethodInfo methodInfo; - if (rank == 2 && type == elementType.MakeArrayType(2)) - methodInfo = typeof(ClonerToExprGenerator).GetPrivateStaticMethod("Clone2DimArrayInternal").MakeGenericMethod(elementType); - else - methodInfo = typeof(ClonerToExprGenerator).GetPrivateStaticMethod("CloneAbstractArrayInternal"); - - var callS = Expression.Call(methodInfo, Expression.Convert(from, type), Expression.Convert(to, type), state, Expression.Constant(isDeep)); + var methodName = "Clone1DimArrayClassInternal"; + if (DeepClonerSafeTypes.CanReturnSameObject(elementType)) methodName = "Clone1DimArraySafeInternal"; + else if (elementType.IsValueType()) methodName = "Clone1DimArrayStructInternal"; + var methodInfo = typeof(ClonerToExprGenerator).GetPrivateStaticMethod(methodName).MakeGenericMethod(elementType); + var callS = Expression.Call(methodInfo, Expression.Convert(from, type), Expression.Convert(to, type), state); return Expression.Lambda(funcType, callS, from, to, state).Compile(); } } - - // when we can't use code generation, we can use these methods - internal static T[] ShallowClone1DimArraySafeInternal(T[] objFrom, T[] objTo) + else { - var l = Math.Min(objFrom.Length, objTo.Length); - Array.Copy(objFrom, objTo, l); - return objTo; + // multidim or not zero-based arrays + MethodInfo methodInfo; + if (rank == 2 && type == elementType.MakeArrayType(2)) + methodInfo = typeof(ClonerToExprGenerator).GetPrivateStaticMethod("Clone2DimArrayInternal").MakeGenericMethod(elementType); + else + methodInfo = typeof(ClonerToExprGenerator).GetPrivateStaticMethod("CloneAbstractArrayInternal"); + + var callS = Expression.Call(methodInfo, Expression.Convert(from, type), Expression.Convert(to, type), state, Expression.Constant(isDeep)); + return Expression.Lambda(funcType, callS, from, to, state).Compile(); } + } - // when we can't use code generation, we can use these methods - internal static T[] Clone1DimArraySafeInternal(T[] objFrom, T[] objTo, DeepCloneState state) + // when we can't use code generation, we can use these methods + internal static T[] ShallowClone1DimArraySafeInternal(T[] objFrom, T[] objTo) + { + var l = Math.Min(objFrom.Length, objTo.Length); + Array.Copy(objFrom, objTo, l); + return objTo; + } + + // when we can't use code generation, we can use these methods + internal static T[] Clone1DimArraySafeInternal(T[] objFrom, T[] objTo, DeepCloneState state) + { + var l = Math.Min(objFrom.Length, objTo.Length); + state.AddKnownRef(objFrom, objTo); + Array.Copy(objFrom, objTo, l); + return objTo; + } + + internal static T[] Clone1DimArrayStructInternal(T[] objFrom, T[] objTo, DeepCloneState state) + { + // not null from called method, but will check it anyway + if (objFrom == null || objTo == null) return null; + var l = Math.Min(objFrom.Length, objTo.Length); + state.AddKnownRef(objFrom, objTo); + var cloner = DeepClonerGenerator.GetClonerForValueType(); + for (var i = 0; i < l; i++) + objTo[i] = cloner(objTo[i], state); + + return objTo; + } + + internal static T[] Clone1DimArrayClassInternal(T[] objFrom, T[] objTo, DeepCloneState state) + { + // not null from called method, but will check it anyway + if (objFrom == null || objTo == null) return null; + var l = Math.Min(objFrom.Length, objTo.Length); + state.AddKnownRef(objFrom, objTo); + for (var i = 0; i < l; i++) + objTo[i] = (T)DeepClonerGenerator.CloneClassInternal(objFrom[i], state); + + return objTo; + } + + internal static T[,] Clone2DimArrayInternal(T[,] objFrom, T[,] objTo, DeepCloneState state, bool isDeep) + { + // not null from called method, but will check it anyway + if (objFrom == null || objTo == null) return null; + if (objFrom.GetLowerBound(0) != 0 || objFrom.GetLowerBound(1) != 0 + || objTo.GetLowerBound(0) != 0 || objTo.GetLowerBound(1) != 0) + return (T[,])CloneAbstractArrayInternal(objFrom, objTo, state, isDeep); + + var l1 = Math.Min(objFrom.GetLength(0), objTo.GetLength(0)); + var l2 = Math.Min(objFrom.GetLength(1), objTo.GetLength(1)); + state.AddKnownRef(objFrom, objTo); + if ((!isDeep || DeepClonerSafeTypes.CanReturnSameObject(typeof(T))) + && objFrom.GetLength(0) == objTo.GetLength(0) + && objFrom.GetLength(1) == objTo.GetLength(1)) { - var l = Math.Min(objFrom.Length, objTo.Length); - state.AddKnownRef(objFrom, objTo); - Array.Copy(objFrom, objTo, l); + Array.Copy(objFrom, objTo, objFrom.Length); return objTo; } - internal static T[] Clone1DimArrayStructInternal(T[] objFrom, T[] objTo, DeepCloneState state) + if (!isDeep) { - // not null from called method, but will check it anyway - if (objFrom == null || objTo == null) return null; - var l = Math.Min(objFrom.Length, objTo.Length); - state.AddKnownRef(objFrom, objTo); - var cloner = DeepClonerGenerator.GetClonerForValueType(); - for (var i = 0; i < l; i++) - objTo[i] = cloner(objTo[i], state); - + for (var i = 0; i < l1; i++) + for (var k = 0; k < l2; k++) + objTo[i, k] = objFrom[i, k]; return objTo; } - internal static T[] Clone1DimArrayClassInternal(T[] objFrom, T[] objTo, DeepCloneState state) + if (typeof(T).IsValueType()) { - // not null from called method, but will check it anyway - if (objFrom == null || objTo == null) return null; - var l = Math.Min(objFrom.Length, objTo.Length); - state.AddKnownRef(objFrom, objTo); - for (var i = 0; i < l; i++) - objTo[i] = (T)DeepClonerGenerator.CloneClassInternal(objFrom[i], state); - - return objTo; + var cloner = DeepClonerGenerator.GetClonerForValueType(); + for (var i = 0; i < l1; i++) + for (var k = 0; k < l2; k++) + objTo[i, k] = cloner(objFrom[i, k], state); } - - internal static T[,] Clone2DimArrayInternal(T[,] objFrom, T[,] objTo, DeepCloneState state, bool isDeep) + else { - // not null from called method, but will check it anyway - if (objFrom == null || objTo == null) return null; - if (objFrom.GetLowerBound(0) != 0 || objFrom.GetLowerBound(1) != 0 - || objTo.GetLowerBound(0) != 0 || objTo.GetLowerBound(1) != 0) - return (T[,])CloneAbstractArrayInternal(objFrom, objTo, state, isDeep); - - var l1 = Math.Min(objFrom.GetLength(0), objTo.GetLength(0)); - var l2 = Math.Min(objFrom.GetLength(1), objTo.GetLength(1)); - state.AddKnownRef(objFrom, objTo); - if ((!isDeep || DeepClonerSafeTypes.CanReturnSameObject(typeof(T))) - && objFrom.GetLength(0) == objTo.GetLength(0) - && objFrom.GetLength(1) == objTo.GetLength(1)) - { - Array.Copy(objFrom, objTo, objFrom.Length); - return objTo; - } - - if (!isDeep) - { - for (var i = 0; i < l1; i++) - for (var k = 0; k < l2; k++) - objTo[i, k] = objFrom[i, k]; - return objTo; - } + for (var i = 0; i < l1; i++) + for (var k = 0; k < l2; k++) + objTo[i, k] = (T)DeepClonerGenerator.CloneClassInternal(objFrom[i, k], state); + } - if (typeof(T).IsValueType()) - { - var cloner = DeepClonerGenerator.GetClonerForValueType(); - for (var i = 0; i < l1; i++) - for (var k = 0; k < l2; k++) - objTo[i, k] = cloner(objFrom[i, k], state); - } - else - { - for (var i = 0; i < l1; i++) - for (var k = 0; k < l2; k++) - objTo[i, k] = (T)DeepClonerGenerator.CloneClassInternal(objFrom[i, k], state); - } + return objTo; + } + // rare cases, very slow cloning. currently it's ok + internal static Array CloneAbstractArrayInternal(Array objFrom, Array objTo, DeepCloneState state, bool isDeep) + { + // not null from called method, but will check it anyway + if (objFrom == null || objTo == null) return null; + var rank = objFrom.Rank; + + if (objTo.Rank != rank) + throw new InvalidOperationException("Invalid rank of target array"); + var lowerBoundsFrom = Enumerable.Range(0, rank).Select(objFrom.GetLowerBound).ToArray(); + var lowerBoundsTo = Enumerable.Range(0, rank).Select(objTo.GetLowerBound).ToArray(); + var lengths = Enumerable.Range(0, rank).Select(x => Math.Min(objFrom.GetLength(x), objTo.GetLength(x))).ToArray(); + var idxesFrom = Enumerable.Range(0, rank).Select(objFrom.GetLowerBound).ToArray(); + var idxesTo = Enumerable.Range(0, rank).Select(objTo.GetLowerBound).ToArray(); + + state.AddKnownRef(objFrom, objTo); + + // unable to copy any element + if (lengths.Any(x => x == 0)) return objTo; - } - // rare cases, very slow cloning. currently it's ok - internal static Array CloneAbstractArrayInternal(Array objFrom, Array objTo, DeepCloneState state, bool isDeep) + while (true) { - // not null from called method, but will check it anyway - if (objFrom == null || objTo == null) return null; - var rank = objFrom.Rank; - - if (objTo.Rank != rank) - throw new InvalidOperationException("Invalid rank of target array"); - var lowerBoundsFrom = Enumerable.Range(0, rank).Select(objFrom.GetLowerBound).ToArray(); - var lowerBoundsTo = Enumerable.Range(0, rank).Select(objTo.GetLowerBound).ToArray(); - var lengths = Enumerable.Range(0, rank).Select(x => Math.Min(objFrom.GetLength(x), objTo.GetLength(x))).ToArray(); - var idxesFrom = Enumerable.Range(0, rank).Select(objFrom.GetLowerBound).ToArray(); - var idxesTo = Enumerable.Range(0, rank).Select(objTo.GetLowerBound).ToArray(); - - state.AddKnownRef(objFrom, objTo); - - // unable to copy any element - if (lengths.Any(x => x == 0)) - return objTo; - + if (isDeep) + objTo.SetValue(DeepClonerGenerator.CloneClassInternal(objFrom.GetValue(idxesFrom), state), idxesTo); + else + objTo.SetValue(objFrom.GetValue(idxesFrom), idxesTo); + var ofs = rank - 1; while (true) { - if (isDeep) - objTo.SetValue(DeepClonerGenerator.CloneClassInternal(objFrom.GetValue(idxesFrom), state), idxesTo); - else - objTo.SetValue(objFrom.GetValue(idxesFrom), idxesTo); - var ofs = rank - 1; - while (true) + idxesFrom[ofs]++; + idxesTo[ofs]++; + if (idxesFrom[ofs] >= lowerBoundsFrom[ofs] + lengths[ofs]) { - idxesFrom[ofs]++; - idxesTo[ofs]++; - if (idxesFrom[ofs] >= lowerBoundsFrom[ofs] + lengths[ofs]) - { - idxesFrom[ofs] = lowerBoundsFrom[ofs]; - idxesTo[ofs] = lowerBoundsTo[ofs]; - ofs--; - if (ofs < 0) return objTo; - } - else - break; + idxesFrom[ofs] = lowerBoundsFrom[ofs]; + idxesTo[ofs] = lowerBoundsTo[ofs]; + ofs--; + if (ofs < 0) return objTo; } + else + break; } } - } + } diff --git a/src/Foundatio/DeepCloner/Helpers/DeepCloneState.cs b/src/Foundatio/DeepCloner/Helpers/DeepCloneState.cs index 2d82c23ba..a203f3fb3 100644 --- a/src/Foundatio/DeepCloner/Helpers/DeepCloneState.cs +++ b/src/Foundatio/DeepCloner/Helpers/DeepCloneState.cs @@ -3,213 +3,212 @@ using System.Collections.Generic; using System.Runtime.CompilerServices; -namespace Foundatio.Force.DeepCloner.Helpers +namespace Foundatio.Force.DeepCloner.Helpers; + +internal class DeepCloneState { - internal class DeepCloneState + private MiniDictionary _loops; + + private readonly object[] _baseFromTo = new object[6]; + + private int _idx; + + public object GetKnownRef(object from) { - private MiniDictionary _loops; + // this is faster than call Dictionary from begin + // also, small poco objects does not have a lot of references + var baseFromTo = _baseFromTo; + if (ReferenceEquals(from, baseFromTo[0])) return baseFromTo[3]; + if (ReferenceEquals(from, baseFromTo[1])) return baseFromTo[4]; + if (ReferenceEquals(from, baseFromTo[2])) return baseFromTo[5]; + if (_loops == null) + return null; + + return _loops.FindEntry(from); + } - private readonly object[] _baseFromTo = new object[6]; + public void AddKnownRef(object from, object to) + { + if (_idx < 3) + { + _baseFromTo[_idx] = from; + _baseFromTo[_idx + 3] = to; + _idx++; + return; + } - private int _idx; + if (_loops == null) + _loops = new MiniDictionary(); + _loops.Insert(from, to); + } - public object GetKnownRef(object from) + private class MiniDictionary + { + private struct Entry { - // this is faster than call Dictionary from begin - // also, small poco objects does not have a lot of references - var baseFromTo = _baseFromTo; - if (ReferenceEquals(from, baseFromTo[0])) return baseFromTo[3]; - if (ReferenceEquals(from, baseFromTo[1])) return baseFromTo[4]; - if (ReferenceEquals(from, baseFromTo[2])) return baseFromTo[5]; - if (_loops == null) - return null; - - return _loops.FindEntry(from); + public int HashCode; + public int Next; + public object Key; + public object Value; } - public void AddKnownRef(object from, object to) + private int[] _buckets; + private Entry[] _entries; + private int _count; + + + public MiniDictionary() : this(5) { - if (_idx < 3) - { - _baseFromTo[_idx] = from; - _baseFromTo[_idx + 3] = to; - _idx++; - return; - } + } - if (_loops == null) - _loops = new MiniDictionary(); - _loops.Insert(from, to); + public MiniDictionary(int capacity) + { + if (capacity > 0) + Initialize(capacity); } - private class MiniDictionary + public object FindEntry(object key) { - private struct Entry + if (_buckets != null) { - public int HashCode; - public int Next; - public object Key; - public object Value; + var hashCode = RuntimeHelpers.GetHashCode(key) & 0x7FFFFFFF; + var entries1 = _entries; + for (var i = _buckets[hashCode % _buckets.Length]; i >= 0; i = entries1[i].Next) + { + if (entries1[i].HashCode == hashCode && ReferenceEquals(entries1[i].Key, key)) + return entries1[i].Value; + } } - private int[] _buckets; - private Entry[] _entries; - private int _count; - + return null; + } - public MiniDictionary() : this(5) + private static readonly int[] _primes = + { + 3, 7, 11, 17, 23, 29, 37, 47, 59, 71, 89, 107, 131, 163, 197, 239, 293, 353, 431, 521, 631, 761, 919, + 1103, 1327, 1597, 1931, 2333, 2801, 3371, 4049, 4861, 5839, 7013, 8419, 10103, 12143, 14591, + 17519, 21023, 25229, 30293, 36353, 43627, 52361, 62851, 75431, 90523, 108631, 130363, 156437, + 187751, 225307, 270371, 324449, 389357, 467237, 560689, 672827, 807403, 968897, 1162687, 1395263, + 1674319, 2009191, 2411033, 2893249, 3471899, 4166287, 4999559, 5999471, 7199369 + }; + + private static int GetPrime(int min) + { + for (var i = 0; i < _primes.Length; i++) { + var prime = _primes[i]; + if (prime >= min) return prime; } - public MiniDictionary(int capacity) + //outside of our predefined table. + //compute the hard way. + for (var i = min | 1; i < int.MaxValue; i += 2) { - if (capacity > 0) - Initialize(capacity); + if (IsPrime(i) && (i - 1) % 101 != 0) + return i; } - public object FindEntry(object key) + return min; + } + + private static bool IsPrime(int candidate) + { + if ((candidate & 1) != 0) { - if (_buckets != null) + var limit = (int)Math.Sqrt(candidate); + for (var divisor = 3; divisor <= limit; divisor += 2) { - var hashCode = RuntimeHelpers.GetHashCode(key) & 0x7FFFFFFF; - var entries1 = _entries; - for (var i = _buckets[hashCode % _buckets.Length]; i >= 0; i = entries1[i].Next) - { - if (entries1[i].HashCode == hashCode && ReferenceEquals(entries1[i].Key, key)) - return entries1[i].Value; - } + if ((candidate % divisor) == 0) + return false; } - return null; + return true; } - private static readonly int[] _primes = - { - 3, 7, 11, 17, 23, 29, 37, 47, 59, 71, 89, 107, 131, 163, 197, 239, 293, 353, 431, 521, 631, 761, 919, - 1103, 1327, 1597, 1931, 2333, 2801, 3371, 4049, 4861, 5839, 7013, 8419, 10103, 12143, 14591, - 17519, 21023, 25229, 30293, 36353, 43627, 52361, 62851, 75431, 90523, 108631, 130363, 156437, - 187751, 225307, 270371, 324449, 389357, 467237, 560689, 672827, 807403, 968897, 1162687, 1395263, - 1674319, 2009191, 2411033, 2893249, 3471899, 4166287, 4999559, 5999471, 7199369 - }; - - private static int GetPrime(int min) - { - for (var i = 0; i < _primes.Length; i++) - { - var prime = _primes[i]; - if (prime >= min) return prime; - } + return candidate == 2; + } - //outside of our predefined table. - //compute the hard way. - for (var i = min | 1; i < int.MaxValue; i += 2) - { - if (IsPrime(i) && (i - 1) % 101 != 0) - return i; - } + private static int ExpandPrime(int oldSize) + { + var newSize = 2 * oldSize; - return min; + if ((uint)newSize > 0x7FEFFFFD && 0x7FEFFFFD > oldSize) + { + return 0x7FEFFFFD; } - private static bool IsPrime(int candidate) - { - if ((candidate & 1) != 0) - { - var limit = (int)Math.Sqrt(candidate); - for (var divisor = 3; divisor <= limit; divisor += 2) - { - if ((candidate % divisor) == 0) - return false; - } - - return true; - } + return GetPrime(newSize); + } - return candidate == 2; - } + private void Initialize(int size) + { + _buckets = new int[size]; + for (int i = 0; i < _buckets.Length; i++) + _buckets[i] = -1; + _entries = new Entry[size]; + } - private static int ExpandPrime(int oldSize) - { - var newSize = 2 * oldSize; + public void Insert(object key, object value) + { + if (_buckets == null) Initialize(0); + var hashCode = RuntimeHelpers.GetHashCode(key) & 0x7FFFFFFF; + var targetBucket = hashCode % _buckets.Length; + + var entries1 = _entries; - if ((uint)newSize > 0x7FEFFFFD && 0x7FEFFFFD > oldSize) + // we're always checking for entry before adding new + // so this loop is useless + /*for (var i = _buckets[targetBucket]; i >= 0; i = entries1[i].Next) + { + if (entries1[i].HashCode == hashCode && ReferenceEquals(entries1[i].Key, key)) { - return 0x7FEFFFFD; + entries1[i].Value = value; + return; } + }*/ - return GetPrime(newSize); - } - - private void Initialize(int size) + if (_count == entries1.Length) { - _buckets = new int[size]; - for (int i = 0; i < _buckets.Length; i++) - _buckets[i] = -1; - _entries = new Entry[size]; + Resize(); + entries1 = _entries; + targetBucket = hashCode % _buckets.Length; } - public void Insert(object key, object value) - { - if (_buckets == null) Initialize(0); - var hashCode = RuntimeHelpers.GetHashCode(key) & 0x7FFFFFFF; - var targetBucket = hashCode % _buckets.Length; - - var entries1 = _entries; - - // we're always checking for entry before adding new - // so this loop is useless - /*for (var i = _buckets[targetBucket]; i >= 0; i = entries1[i].Next) - { - if (entries1[i].HashCode == hashCode && ReferenceEquals(entries1[i].Key, key)) - { - entries1[i].Value = value; - return; - } - }*/ - - if (_count == entries1.Length) - { - Resize(); - entries1 = _entries; - targetBucket = hashCode % _buckets.Length; - } + var index = _count; + _count++; - var index = _count; - _count++; + entries1[index].HashCode = hashCode; + entries1[index].Next = _buckets[targetBucket]; + entries1[index].Key = key; + entries1[index].Value = value; + _buckets[targetBucket] = index; + } - entries1[index].HashCode = hashCode; - entries1[index].Next = _buckets[targetBucket]; - entries1[index].Key = key; - entries1[index].Value = value; - _buckets[targetBucket] = index; - } + private void Resize() + { + Resize(ExpandPrime(_count)); + } - private void Resize() - { - Resize(ExpandPrime(_count)); - } + private void Resize(int newSize) + { + var newBuckets = new int[newSize]; + for (int i = 0; i < newBuckets.Length; i++) + newBuckets[i] = -1; + var newEntries = new Entry[newSize]; + Array.Copy(_entries, 0, newEntries, 0, _count); - private void Resize(int newSize) + for (var i = 0; i < _count; i++) { - var newBuckets = new int[newSize]; - for (int i = 0; i < newBuckets.Length; i++) - newBuckets[i] = -1; - var newEntries = new Entry[newSize]; - Array.Copy(_entries, 0, newEntries, 0, _count); - - for (var i = 0; i < _count; i++) + if (newEntries[i].HashCode >= 0) { - if (newEntries[i].HashCode >= 0) - { - var bucket = newEntries[i].HashCode % newSize; - newEntries[i].Next = newBuckets[bucket]; - newBuckets[bucket] = i; - } + var bucket = newEntries[i].HashCode % newSize; + newEntries[i].Next = newBuckets[bucket]; + newBuckets[bucket] = i; } - - _buckets = newBuckets; - _entries = newEntries; } + + _buckets = newBuckets; + _entries = newEntries; } } } diff --git a/src/Foundatio/DeepCloner/Helpers/DeepClonerCache.cs b/src/Foundatio/DeepCloner/Helpers/DeepClonerCache.cs index 4c8d1781f..56eb8896e 100644 --- a/src/Foundatio/DeepCloner/Helpers/DeepClonerCache.cs +++ b/src/Foundatio/DeepCloner/Helpers/DeepClonerCache.cs @@ -1,97 +1,96 @@ using System; using System.Collections.Concurrent; -namespace Foundatio.Force.DeepCloner.Helpers -{ - internal static class DeepClonerCache - { - private static readonly ConcurrentDictionary _typeCache = new(); +namespace Foundatio.Force.DeepCloner.Helpers; - private static readonly ConcurrentDictionary _typeCacheDeepTo = new(); +internal static class DeepClonerCache +{ + private static readonly ConcurrentDictionary _typeCache = new(); - private static readonly ConcurrentDictionary _typeCacheShallowTo = new(); + private static readonly ConcurrentDictionary _typeCacheDeepTo = new(); - private static readonly ConcurrentDictionary _structAsObjectCache = new(); + private static readonly ConcurrentDictionary _typeCacheShallowTo = new(); - private static readonly ConcurrentDictionary, object> _typeConvertCache = new(); + private static readonly ConcurrentDictionary _structAsObjectCache = new(); - public static object GetOrAddClass(Type type, Func adder) - { - // return _typeCache.GetOrAdd(type, x => adder(x)); + private static readonly ConcurrentDictionary, object> _typeConvertCache = new(); - // this implementation is slightly faster than getoradd - object value; - if (_typeCache.TryGetValue(type, out value)) return value; - - // will lock by type object to ensure only one type generator is generated simultaneously - lock (type) - { - value = _typeCache.GetOrAdd(type, t => adder(t)); - } + public static object GetOrAddClass(Type type, Func adder) + { + // return _typeCache.GetOrAdd(type, x => adder(x)); - return value; - } + // this implementation is slightly faster than getoradd + object value; + if (_typeCache.TryGetValue(type, out value)) return value; - public static object GetOrAddDeepClassTo(Type type, Func adder) + // will lock by type object to ensure only one type generator is generated simultaneously + lock (type) { - object value; - if (_typeCacheDeepTo.TryGetValue(type, out value)) return value; + value = _typeCache.GetOrAdd(type, t => adder(t)); + } - // will lock by type object to ensure only one type generator is generated simultaneously - lock (type) - { - value = _typeCacheDeepTo.GetOrAdd(type, t => adder(t)); - } + return value; + } - return value; - } + public static object GetOrAddDeepClassTo(Type type, Func adder) + { + object value; + if (_typeCacheDeepTo.TryGetValue(type, out value)) return value; - public static object GetOrAddShallowClassTo(Type type, Func adder) + // will lock by type object to ensure only one type generator is generated simultaneously + lock (type) { - object value; - if (_typeCacheShallowTo.TryGetValue(type, out value)) return value; + value = _typeCacheDeepTo.GetOrAdd(type, t => adder(t)); + } - // will lock by type object to ensure only one type generator is generated simultaneously - lock (type) - { - value = _typeCacheShallowTo.GetOrAdd(type, t => adder(t)); - } + return value; + } - return value; - } + public static object GetOrAddShallowClassTo(Type type, Func adder) + { + object value; + if (_typeCacheShallowTo.TryGetValue(type, out value)) return value; - public static object GetOrAddStructAsObject(Type type, Func adder) + // will lock by type object to ensure only one type generator is generated simultaneously + lock (type) { - // return _typeCache.GetOrAdd(type, x => adder(x)); + value = _typeCacheShallowTo.GetOrAdd(type, t => adder(t)); + } - // this implementation is slightly faster than getoradd - object value; - if (_structAsObjectCache.TryGetValue(type, out value)) return value; + return value; + } - // will lock by type object to ensure only one type generator is generated simultaneously - lock (type) - { - value = _structAsObjectCache.GetOrAdd(type, t => adder(t)); - } + public static object GetOrAddStructAsObject(Type type, Func adder) + { + // return _typeCache.GetOrAdd(type, x => adder(x)); - return value; - } + // this implementation is slightly faster than getoradd + object value; + if (_structAsObjectCache.TryGetValue(type, out value)) return value; - public static T GetOrAddConvertor(Type from, Type to, Func adder) + // will lock by type object to ensure only one type generator is generated simultaneously + lock (type) { - return (T)_typeConvertCache.GetOrAdd(new Tuple(from, to), (tuple) => adder(tuple.Item1, tuple.Item2)); + value = _structAsObjectCache.GetOrAdd(type, t => adder(t)); } - /// - /// This method can be used when we switch between safe / unsafe variants (for testing) - /// - public static void ClearCache() - { - _typeCache.Clear(); - _typeCacheDeepTo.Clear(); - _typeCacheShallowTo.Clear(); - _structAsObjectCache.Clear(); - _typeConvertCache.Clear(); - } + return value; + } + + public static T GetOrAddConvertor(Type from, Type to, Func adder) + { + return (T)_typeConvertCache.GetOrAdd(new Tuple(from, to), (tuple) => adder(tuple.Item1, tuple.Item2)); + } + + /// + /// This method can be used when we switch between safe / unsafe variants (for testing) + /// + public static void ClearCache() + { + _typeCache.Clear(); + _typeCacheDeepTo.Clear(); + _typeCacheShallowTo.Clear(); + _structAsObjectCache.Clear(); + _typeConvertCache.Clear(); } } diff --git a/src/Foundatio/DeepCloner/Helpers/DeepClonerExprGenerator.cs b/src/Foundatio/DeepCloner/Helpers/DeepClonerExprGenerator.cs index 80230624b..8eaf1b4b7 100644 --- a/src/Foundatio/DeepCloner/Helpers/DeepClonerExprGenerator.cs +++ b/src/Foundatio/DeepCloner/Helpers/DeepClonerExprGenerator.cs @@ -6,259 +6,258 @@ using System.Linq.Expressions; using System.Reflection; -namespace Foundatio.Force.DeepCloner.Helpers +namespace Foundatio.Force.DeepCloner.Helpers; + +internal static class DeepClonerExprGenerator { - internal static class DeepClonerExprGenerator - { - private static readonly ConcurrentDictionary _readonlyFields = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary _readonlyFields = new ConcurrentDictionary(); - private static readonly bool _canFastCopyReadonlyFields = false; + private static readonly bool _canFastCopyReadonlyFields = false; - private static readonly MethodInfo _fieldSetMethod; - static DeepClonerExprGenerator() + private static readonly MethodInfo _fieldSetMethod; + static DeepClonerExprGenerator() + { + try { - try - { - typeof(DeepClonerExprGenerator).GetPrivateStaticField(nameof(_canFastCopyReadonlyFields)).SetValue(null, true); + typeof(DeepClonerExprGenerator).GetPrivateStaticField(nameof(_canFastCopyReadonlyFields)).SetValue(null, true); #if NETCORE13 _fieldSetMethod = typeof(FieldInfo).GetRuntimeMethod("SetValue", new[] { typeof(object), typeof(object) }); #else - _fieldSetMethod = typeof(FieldInfo).GetMethod("SetValue", new[] { typeof(object), typeof(object) }); + _fieldSetMethod = typeof(FieldInfo).GetMethod("SetValue", new[] { typeof(object), typeof(object) }); #endif - if (_fieldSetMethod == null) - throw new ArgumentNullException(); - } - catch (Exception) - { - // cannot - } + if (_fieldSetMethod == null) + throw new ArgumentNullException(); } - - internal static object GenerateClonerInternal(Type realType, bool asObject) + catch (Exception) { - return GenerateProcessMethod(realType, asObject && realType.IsValueType()); + // cannot } + } - private static FieldInfo _attributesFieldInfo = typeof(FieldInfo).GetPrivateField("m_fieldAttributes"); + internal static object GenerateClonerInternal(Type realType, bool asObject) + { + return GenerateProcessMethod(realType, asObject && realType.IsValueType()); + } - // today, I found that it not required to do such complex things. Just SetValue is enough - // is it new runtime changes, or I made incorrect assumptions eariler - // slow, but hardcore method to set readonly field - internal static void ForceSetField(FieldInfo field, object obj, object value) + private static FieldInfo _attributesFieldInfo = typeof(FieldInfo).GetPrivateField("m_fieldAttributes"); + + // today, I found that it not required to do such complex things. Just SetValue is enough + // is it new runtime changes, or I made incorrect assumptions eariler + // slow, but hardcore method to set readonly field + internal static void ForceSetField(FieldInfo field, object obj, object value) + { + var fieldInfo = field.GetType().GetPrivateField("m_fieldAttributes"); + + // TODO: think about it + // nothing to do :( we should a throw an exception, but it is no good for user + if (fieldInfo == null) + return; + var ov = fieldInfo.GetValue(field); + if (!(ov is FieldAttributes)) + return; + var v = (FieldAttributes)ov; + + // protect from parallel execution, when first thread set field readonly back, and second set it to write value + lock (fieldInfo) { - var fieldInfo = field.GetType().GetPrivateField("m_fieldAttributes"); - - // TODO: think about it - // nothing to do :( we should a throw an exception, but it is no good for user - if (fieldInfo == null) - return; - var ov = fieldInfo.GetValue(field); - if (!(ov is FieldAttributes)) - return; - var v = (FieldAttributes)ov; - - // protect from parallel execution, when first thread set field readonly back, and second set it to write value - lock (fieldInfo) - { - fieldInfo.SetValue(field, v & ~FieldAttributes.InitOnly); - field.SetValue(obj, value); - fieldInfo.SetValue(field, v | FieldAttributes.InitOnly); - } + fieldInfo.SetValue(field, v & ~FieldAttributes.InitOnly); + field.SetValue(obj, value); + fieldInfo.SetValue(field, v | FieldAttributes.InitOnly); } + } - private static object GenerateProcessMethod(Type type, bool unboxStruct) + private static object GenerateProcessMethod(Type type, bool unboxStruct) + { + if (type.IsArray) { - if (type.IsArray) - { - return GenerateProcessArrayMethod(type); - } + return GenerateProcessArrayMethod(type); + } - if (type.FullName != null && type.FullName.StartsWith("System.Tuple`")) + if (type.FullName != null && type.FullName.StartsWith("System.Tuple`")) + { + // if not safe type it is no guarantee that some type will contain reference to + // this tuple. In usual way, we're creating new object, setting reference for it + // and filling data. For tuple, we will fill data before creating object + // (in constructor arguments) + var genericArguments = type.GenericArguments(); + // current tuples contain only 8 arguments, but may be in future... + // we'll write code that works with it + if (genericArguments.Length < 10 && genericArguments.All(DeepClonerSafeTypes.CanReturnSameObject)) { - // if not safe type it is no guarantee that some type will contain reference to - // this tuple. In usual way, we're creating new object, setting reference for it - // and filling data. For tuple, we will fill data before creating object - // (in constructor arguments) - var genericArguments = type.GenericArguments(); - // current tuples contain only 8 arguments, but may be in future... - // we'll write code that works with it - if (genericArguments.Length < 10 && genericArguments.All(DeepClonerSafeTypes.CanReturnSameObject)) - { - return GenerateProcessTupleMethod(type); - } + return GenerateProcessTupleMethod(type); } + } - var methodType = unboxStruct || type.IsClass() ? typeof(object) : type; + var methodType = unboxStruct || type.IsClass() ? typeof(object) : type; - var expressionList = new List(); + var expressionList = new List(); - ParameterExpression from = Expression.Parameter(methodType); - var fromLocal = from; - var toLocal = Expression.Variable(type); - var state = Expression.Parameter(typeof(DeepCloneState)); + ParameterExpression from = Expression.Parameter(methodType); + var fromLocal = from; + var toLocal = Expression.Variable(type); + var state = Expression.Parameter(typeof(DeepCloneState)); - if (!type.IsValueType()) - { - var methodInfo = typeof(object).GetPrivateMethod("MemberwiseClone"); + if (!type.IsValueType()) + { + var methodInfo = typeof(object).GetPrivateMethod("MemberwiseClone"); - // to = (T)from.MemberwiseClone() - expressionList.Add(Expression.Assign(toLocal, Expression.Convert(Expression.Call(from, methodInfo), type))); + // to = (T)from.MemberwiseClone() + expressionList.Add(Expression.Assign(toLocal, Expression.Convert(Expression.Call(from, methodInfo), type))); - fromLocal = Expression.Variable(type); - // fromLocal = (T)from - expressionList.Add(Expression.Assign(fromLocal, Expression.Convert(from, type))); + fromLocal = Expression.Variable(type); + // fromLocal = (T)from + expressionList.Add(Expression.Assign(fromLocal, Expression.Convert(from, type))); - // added from -> to binding to ensure reference loop handling - // structs cannot loop here - // state.AddKnownRef(from, to) - expressionList.Add(Expression.Call(state, typeof(DeepCloneState).GetMethod("AddKnownRef"), from, toLocal)); + // added from -> to binding to ensure reference loop handling + // structs cannot loop here + // state.AddKnownRef(from, to) + expressionList.Add(Expression.Call(state, typeof(DeepCloneState).GetMethod("AddKnownRef"), from, toLocal)); + } + else + { + if (unboxStruct) + { + // toLocal = (T)from; + expressionList.Add(Expression.Assign(toLocal, Expression.Unbox(from, type))); + fromLocal = Expression.Variable(type); + // fromLocal = toLocal; // structs, it is ok to copy + expressionList.Add(Expression.Assign(fromLocal, toLocal)); } else { - if (unboxStruct) - { - // toLocal = (T)from; - expressionList.Add(Expression.Assign(toLocal, Expression.Unbox(from, type))); - fromLocal = Expression.Variable(type); - // fromLocal = toLocal; // structs, it is ok to copy - expressionList.Add(Expression.Assign(fromLocal, toLocal)); - } - else - { - // toLocal = from - expressionList.Add(Expression.Assign(toLocal, from)); - } + // toLocal = from + expressionList.Add(Expression.Assign(toLocal, from)); } + } - List fi = new List(); - var tp = type; - do - { + List fi = new List(); + var tp = type; + do + { #if !NETCORE // don't do anything with this dark magic! if (tp == typeof(ContextBoundObject)) break; #else - if (tp.Name == "ContextBoundObject") break; + if (tp.Name == "ContextBoundObject") break; #endif - fi.AddRange(tp.GetDeclaredFields()); - tp = tp.BaseType(); - } - while (tp != null); + fi.AddRange(tp.GetDeclaredFields()); + tp = tp.BaseType(); + } + while (tp != null); - foreach (var fieldInfo in fi) + foreach (var fieldInfo in fi) + { + if (!DeepClonerSafeTypes.CanReturnSameObject(fieldInfo.FieldType)) { - if (!DeepClonerSafeTypes.CanReturnSameObject(fieldInfo.FieldType)) + var methodInfo = fieldInfo.FieldType.IsValueType() + ? typeof(DeepClonerGenerator).GetPrivateStaticMethod("CloneStructInternal") + .MakeGenericMethod(fieldInfo.FieldType) + : typeof(DeepClonerGenerator).GetPrivateStaticMethod("CloneClassInternal"); + + var get = Expression.Field(fromLocal, fieldInfo); + + // toLocal.Field = Clone...Internal(fromLocal.Field) + var call = (Expression)Expression.Call(methodInfo, get, state); + if (!fieldInfo.FieldType.IsValueType()) + call = Expression.Convert(call, fieldInfo.FieldType); + + // should handle specially + // todo: think about optimization, but it rare case + var isReadonly = _readonlyFields.GetOrAdd(fieldInfo, f => f.IsInitOnly); + if (isReadonly) { - var methodInfo = fieldInfo.FieldType.IsValueType() - ? typeof(DeepClonerGenerator).GetPrivateStaticMethod("CloneStructInternal") - .MakeGenericMethod(fieldInfo.FieldType) - : typeof(DeepClonerGenerator).GetPrivateStaticMethod("CloneClassInternal"); - - var get = Expression.Field(fromLocal, fieldInfo); - - // toLocal.Field = Clone...Internal(fromLocal.Field) - var call = (Expression)Expression.Call(methodInfo, get, state); - if (!fieldInfo.FieldType.IsValueType()) - call = Expression.Convert(call, fieldInfo.FieldType); - - // should handle specially - // todo: think about optimization, but it rare case - var isReadonly = _readonlyFields.GetOrAdd(fieldInfo, f => f.IsInitOnly); - if (isReadonly) + if (_canFastCopyReadonlyFields) { - if (_canFastCopyReadonlyFields) - { - expressionList.Add(Expression.Call( - Expression.Constant(fieldInfo), - _fieldSetMethod, - Expression.Convert(toLocal, typeof(object)), - Expression.Convert(call, typeof(object)))); - } - else - { - var setMethod = typeof(DeepClonerExprGenerator).GetPrivateStaticMethod("ForceSetField"); - expressionList.Add(Expression.Call(setMethod, Expression.Constant(fieldInfo), Expression.Convert(toLocal, typeof(object)), Expression.Convert(call, typeof(object)))); - } + expressionList.Add(Expression.Call( + Expression.Constant(fieldInfo), + _fieldSetMethod, + Expression.Convert(toLocal, typeof(object)), + Expression.Convert(call, typeof(object)))); } else { - expressionList.Add(Expression.Assign(Expression.Field(toLocal, fieldInfo), call)); + var setMethod = typeof(DeepClonerExprGenerator).GetPrivateStaticMethod("ForceSetField"); + expressionList.Add(Expression.Call(setMethod, Expression.Constant(fieldInfo), Expression.Convert(toLocal, typeof(object)), Expression.Convert(call, typeof(object)))); } } + else + { + expressionList.Add(Expression.Assign(Expression.Field(toLocal, fieldInfo), call)); + } } + } - expressionList.Add(Expression.Convert(toLocal, methodType)); + expressionList.Add(Expression.Convert(toLocal, methodType)); - var funcType = typeof(Func<,,>).MakeGenericType(methodType, typeof(DeepCloneState), methodType); + var funcType = typeof(Func<,,>).MakeGenericType(methodType, typeof(DeepCloneState), methodType); - var blockParams = new List(); - if (from != fromLocal) blockParams.Add(fromLocal); - blockParams.Add(toLocal); + var blockParams = new List(); + if (from != fromLocal) blockParams.Add(fromLocal); + blockParams.Add(toLocal); - return Expression.Lambda(funcType, Expression.Block(blockParams, expressionList), from, state).Compile(); - } + return Expression.Lambda(funcType, Expression.Block(blockParams, expressionList), from, state).Compile(); + } - private static object GenerateProcessArrayMethod(Type type) - { - var elementType = type.GetElementType(); - var rank = type.GetArrayRank(); + private static object GenerateProcessArrayMethod(Type type) + { + var elementType = type.GetElementType(); + var rank = type.GetArrayRank(); - MethodInfo methodInfo; + MethodInfo methodInfo; - // multidim or not zero-based arrays - if (rank != 1 || type != elementType.MakeArrayType()) + // multidim or not zero-based arrays + if (rank != 1 || type != elementType.MakeArrayType()) + { + if (rank == 2 && type == elementType.MakeArrayType(2)) { - if (rank == 2 && type == elementType.MakeArrayType(2)) - { - // small optimization for 2 dim arrays - methodInfo = typeof(DeepClonerGenerator).GetPrivateStaticMethod("Clone2DimArrayInternal").MakeGenericMethod(elementType); - } - else - { - methodInfo = typeof(DeepClonerGenerator).GetPrivateStaticMethod("CloneAbstractArrayInternal"); - } + // small optimization for 2 dim arrays + methodInfo = typeof(DeepClonerGenerator).GetPrivateStaticMethod("Clone2DimArrayInternal").MakeGenericMethod(elementType); } else { - var methodName = "Clone1DimArrayClassInternal"; - if (DeepClonerSafeTypes.CanReturnSameObject(elementType)) methodName = "Clone1DimArraySafeInternal"; - else if (elementType.IsValueType()) methodName = "Clone1DimArrayStructInternal"; - methodInfo = typeof(DeepClonerGenerator).GetPrivateStaticMethod(methodName).MakeGenericMethod(elementType); + methodInfo = typeof(DeepClonerGenerator).GetPrivateStaticMethod("CloneAbstractArrayInternal"); } + } + else + { + var methodName = "Clone1DimArrayClassInternal"; + if (DeepClonerSafeTypes.CanReturnSameObject(elementType)) methodName = "Clone1DimArraySafeInternal"; + else if (elementType.IsValueType()) methodName = "Clone1DimArrayStructInternal"; + methodInfo = typeof(DeepClonerGenerator).GetPrivateStaticMethod(methodName).MakeGenericMethod(elementType); + } - ParameterExpression from = Expression.Parameter(typeof(object)); - var state = Expression.Parameter(typeof(DeepCloneState)); - var call = Expression.Call(methodInfo, Expression.Convert(from, type), state); + ParameterExpression from = Expression.Parameter(typeof(object)); + var state = Expression.Parameter(typeof(DeepCloneState)); + var call = Expression.Call(methodInfo, Expression.Convert(from, type), state); - var funcType = typeof(Func<,,>).MakeGenericType(typeof(object), typeof(DeepCloneState), typeof(object)); + var funcType = typeof(Func<,,>).MakeGenericType(typeof(object), typeof(DeepCloneState), typeof(object)); - return Expression.Lambda(funcType, call, from, state).Compile(); - } + return Expression.Lambda(funcType, call, from, state).Compile(); + } - private static object GenerateProcessTupleMethod(Type type) - { - ParameterExpression from = Expression.Parameter(typeof(object)); - var state = Expression.Parameter(typeof(DeepCloneState)); + private static object GenerateProcessTupleMethod(Type type) + { + ParameterExpression from = Expression.Parameter(typeof(object)); + var state = Expression.Parameter(typeof(DeepCloneState)); - var local = Expression.Variable(type); - var assign = Expression.Assign(local, Expression.Convert(from, type)); + var local = Expression.Variable(type); + var assign = Expression.Assign(local, Expression.Convert(from, type)); - var funcType = typeof(Func); + var funcType = typeof(Func); - var tupleLength = type.GenericArguments().Length; + var tupleLength = type.GenericArguments().Length; - var constructor = Expression.Assign(local, Expression.New(type.GetPublicConstructors().First(x => x.GetParameters().Length == tupleLength), - type.GetPublicProperties().OrderBy(x => x.Name) - .Where(x => x.CanRead && x.Name.StartsWith("Item") && char.IsDigit(x.Name[4])) - .Select(x => Expression.Property(local, x.Name)))); + var constructor = Expression.Assign(local, Expression.New(type.GetPublicConstructors().First(x => x.GetParameters().Length == tupleLength), + type.GetPublicProperties().OrderBy(x => x.Name) + .Where(x => x.CanRead && x.Name.StartsWith("Item") && char.IsDigit(x.Name[4])) + .Select(x => Expression.Property(local, x.Name)))); - return Expression.Lambda(funcType, Expression.Block(new[] { local }, + return Expression.Lambda(funcType, Expression.Block(new[] { local }, assign, constructor, Expression.Call(state, typeof(DeepCloneState).GetMethod("AddKnownRef"), from, local), - from), - from, state).Compile(); - } - + from), + from, state).Compile(); } + } diff --git a/src/Foundatio/DeepCloner/Helpers/DeepClonerGenerator.cs b/src/Foundatio/DeepCloner/Helpers/DeepClonerGenerator.cs index 963a35918..f72f7b52c 100644 --- a/src/Foundatio/DeepCloner/Helpers/DeepClonerGenerator.cs +++ b/src/Foundatio/DeepCloner/Helpers/DeepClonerGenerator.cs @@ -2,231 +2,230 @@ using System; using System.Linq; -namespace Foundatio.Force.DeepCloner.Helpers +namespace Foundatio.Force.DeepCloner.Helpers; + +internal static class DeepClonerGenerator { - internal static class DeepClonerGenerator + public static T CloneObject(T obj) { - public static T CloneObject(T obj) + if (obj is ValueType) { - if (obj is ValueType) + var type = obj.GetType(); + if (typeof(T) == type) { - var type = obj.GetType(); - if (typeof(T) == type) - { - if (DeepClonerSafeTypes.CanReturnSameObject(type)) - return obj; + if (DeepClonerSafeTypes.CanReturnSameObject(type)) + return obj; - return CloneStructInternal(obj, new DeepCloneState()); - } + return CloneStructInternal(obj, new DeepCloneState()); } - - return (T)CloneClassRoot(obj); } - private static object CloneClassRoot(object obj) - { - if (obj == null) - return null; + return (T)CloneClassRoot(obj); + } - var cloner = (Func)DeepClonerCache.GetOrAddClass(obj.GetType(), t => GenerateCloner(t, true)); + private static object CloneClassRoot(object obj) + { + if (obj == null) + return null; - // null -> should return same type - if (cloner == null) - return obj; + var cloner = (Func)DeepClonerCache.GetOrAddClass(obj.GetType(), t => GenerateCloner(t, true)); - return cloner(obj, new DeepCloneState()); - } + // null -> should return same type + if (cloner == null) + return obj; - internal static object CloneClassInternal(object obj, DeepCloneState state) - { - if (obj == null) - return null; + return cloner(obj, new DeepCloneState()); + } - var cloner = (Func)DeepClonerCache.GetOrAddClass(obj.GetType(), t => GenerateCloner(t, true)); + internal static object CloneClassInternal(object obj, DeepCloneState state) + { + if (obj == null) + return null; - // safe object - if (cloner == null) - return obj; + var cloner = (Func)DeepClonerCache.GetOrAddClass(obj.GetType(), t => GenerateCloner(t, true)); - // loop - var knownRef = state.GetKnownRef(obj); - if (knownRef != null) - return knownRef; + // safe object + if (cloner == null) + return obj; - return cloner(obj, state); - } + // loop + var knownRef = state.GetKnownRef(obj); + if (knownRef != null) + return knownRef; - private static T CloneStructInternal(T obj, DeepCloneState state) // where T : struct - { - // no loops, no nulls, no inheritance - var cloner = GetClonerForValueType(); + return cloner(obj, state); + } + + private static T CloneStructInternal(T obj, DeepCloneState state) // where T : struct + { + // no loops, no nulls, no inheritance + var cloner = GetClonerForValueType(); - // safe ojbect - if (cloner == null) - return obj; + // safe ojbect + if (cloner == null) + return obj; - return cloner(obj, state); - } + return cloner(obj, state); + } - // when we can't use code generation, we can use these methods - internal static T[] Clone1DimArraySafeInternal(T[] obj, DeepCloneState state) + // when we can't use code generation, we can use these methods + internal static T[] Clone1DimArraySafeInternal(T[] obj, DeepCloneState state) + { + var l = obj.Length; + var outArray = new T[l]; + state.AddKnownRef(obj, outArray); + Array.Copy(obj, outArray, obj.Length); + return outArray; + } + + internal static T[] Clone1DimArrayStructInternal(T[] obj, DeepCloneState state) + { + // not null from called method, but will check it anyway + if (obj == null) return null; + var l = obj.Length; + var outArray = new T[l]; + state.AddKnownRef(obj, outArray); + var cloner = GetClonerForValueType(); + for (var i = 0; i < l; i++) + outArray[i] = cloner(obj[i], state); + + return outArray; + } + + internal static T[] Clone1DimArrayClassInternal(T[] obj, DeepCloneState state) + { + // not null from called method, but will check it anyway + if (obj == null) return null; + var l = obj.Length; + var outArray = new T[l]; + state.AddKnownRef(obj, outArray); + for (var i = 0; i < l; i++) + outArray[i] = (T)CloneClassInternal(obj[i], state); + + return outArray; + } + + // relatively frequent case. specially handled + internal static T[,] Clone2DimArrayInternal(T[,] obj, DeepCloneState state) + { + // not null from called method, but will check it anyway + if (obj == null) return null; + + // we cannot determine by type multidim arrays (one dimension is possible) + // so, will check for index here + var lb1 = obj.GetLowerBound(0); + var lb2 = obj.GetLowerBound(1); + if (lb1 != 0 || lb2 != 0) + return (T[,])CloneAbstractArrayInternal(obj, state); + + var l1 = obj.GetLength(0); + var l2 = obj.GetLength(1); + var outArray = new T[l1, l2]; + state.AddKnownRef(obj, outArray); + if (DeepClonerSafeTypes.CanReturnSameObject(typeof(T))) { - var l = obj.Length; - var outArray = new T[l]; - state.AddKnownRef(obj, outArray); Array.Copy(obj, outArray, obj.Length); return outArray; } - internal static T[] Clone1DimArrayStructInternal(T[] obj, DeepCloneState state) + if (typeof(T).IsValueType()) { - // not null from called method, but will check it anyway - if (obj == null) return null; - var l = obj.Length; - var outArray = new T[l]; - state.AddKnownRef(obj, outArray); var cloner = GetClonerForValueType(); - for (var i = 0; i < l; i++) - outArray[i] = cloner(obj[i], state); - - return outArray; + for (var i = 0; i < l1; i++) + for (var k = 0; k < l2; k++) + outArray[i, k] = cloner(obj[i, k], state); } - - internal static T[] Clone1DimArrayClassInternal(T[] obj, DeepCloneState state) + else { - // not null from called method, but will check it anyway - if (obj == null) return null; - var l = obj.Length; - var outArray = new T[l]; - state.AddKnownRef(obj, outArray); - for (var i = 0; i < l; i++) - outArray[i] = (T)CloneClassInternal(obj[i], state); - - return outArray; + for (var i = 0; i < l1; i++) + for (var k = 0; k < l2; k++) + outArray[i, k] = (T)CloneClassInternal(obj[i, k], state); } - // relatively frequent case. specially handled - internal static T[,] Clone2DimArrayInternal(T[,] obj, DeepCloneState state) - { - // not null from called method, but will check it anyway - if (obj == null) return null; - - // we cannot determine by type multidim arrays (one dimension is possible) - // so, will check for index here - var lb1 = obj.GetLowerBound(0); - var lb2 = obj.GetLowerBound(1); - if (lb1 != 0 || lb2 != 0) - return (T[,])CloneAbstractArrayInternal(obj, state); - - var l1 = obj.GetLength(0); - var l2 = obj.GetLength(1); - var outArray = new T[l1, l2]; - state.AddKnownRef(obj, outArray); - if (DeepClonerSafeTypes.CanReturnSameObject(typeof(T))) - { - Array.Copy(obj, outArray, obj.Length); - return outArray; - } - - if (typeof(T).IsValueType()) - { - var cloner = GetClonerForValueType(); - for (var i = 0; i < l1; i++) - for (var k = 0; k < l2; k++) - outArray[i, k] = cloner(obj[i, k], state); - } - else - { - for (var i = 0; i < l1; i++) - for (var k = 0; k < l2; k++) - outArray[i, k] = (T)CloneClassInternal(obj[i, k], state); - } + return outArray; + } - return outArray; - } + // rare cases, very slow cloning. currently it's ok + internal static Array CloneAbstractArrayInternal(Array obj, DeepCloneState state) + { + // not null from called method, but will check it anyway + if (obj == null) return null; + var rank = obj.Rank; - // rare cases, very slow cloning. currently it's ok - internal static Array CloneAbstractArrayInternal(Array obj, DeepCloneState state) - { - // not null from called method, but will check it anyway - if (obj == null) return null; - var rank = obj.Rank; + var lengths = Enumerable.Range(0, rank).Select(obj.GetLength).ToArray(); - var lengths = Enumerable.Range(0, rank).Select(obj.GetLength).ToArray(); + var lowerBounds = Enumerable.Range(0, rank).Select(obj.GetLowerBound).ToArray(); + var idxes = Enumerable.Range(0, rank).Select(obj.GetLowerBound).ToArray(); - var lowerBounds = Enumerable.Range(0, rank).Select(obj.GetLowerBound).ToArray(); - var idxes = Enumerable.Range(0, rank).Select(obj.GetLowerBound).ToArray(); + var elementType = obj.GetType().GetElementType(); + var outArray = Array.CreateInstance(elementType, lengths, lowerBounds); - var elementType = obj.GetType().GetElementType(); - var outArray = Array.CreateInstance(elementType, lengths, lowerBounds); + state.AddKnownRef(obj, outArray); - state.AddKnownRef(obj, outArray); + // we're unable to set any value to this array, so, just return it + if (lengths.Any(x => x == 0)) + return outArray; - // we're unable to set any value to this array, so, just return it - if (lengths.Any(x => x == 0)) - return outArray; + if (DeepClonerSafeTypes.CanReturnSameObject(elementType)) + { + Array.Copy(obj, outArray, obj.Length); + return outArray; + } - if (DeepClonerSafeTypes.CanReturnSameObject(elementType)) - { - Array.Copy(obj, outArray, obj.Length); - return outArray; - } + var ofs = rank - 1; + while (true) + { + outArray.SetValue(CloneClassInternal(obj.GetValue(idxes), state), idxes); + idxes[ofs]++; - var ofs = rank - 1; - while (true) + if (idxes[ofs] >= lowerBounds[ofs] + lengths[ofs]) { - outArray.SetValue(CloneClassInternal(obj.GetValue(idxes), state), idxes); - idxes[ofs]++; - - if (idxes[ofs] >= lowerBounds[ofs] + lengths[ofs]) + do { - do - { - if (ofs == 0) return outArray; - idxes[ofs] = lowerBounds[ofs]; - ofs--; - idxes[ofs]++; - } while (idxes[ofs] >= lowerBounds[ofs] + lengths[ofs]); - - ofs = rank - 1; - } + if (ofs == 0) return outArray; + idxes[ofs] = lowerBounds[ofs]; + ofs--; + idxes[ofs]++; + } while (idxes[ofs] >= lowerBounds[ofs] + lengths[ofs]); + + ofs = rank - 1; } } + } - internal static Func GetClonerForValueType() - { - return (Func)DeepClonerCache.GetOrAddStructAsObject(typeof(T), t => GenerateCloner(t, false)); - } + internal static Func GetClonerForValueType() + { + return (Func)DeepClonerCache.GetOrAddStructAsObject(typeof(T), t => GenerateCloner(t, false)); + } - private static object GenerateCloner(Type t, bool asObject) - { - if (DeepClonerSafeTypes.CanReturnSameObject(t) && (asObject && !t.IsValueType())) - return null; + private static object GenerateCloner(Type t, bool asObject) + { + if (DeepClonerSafeTypes.CanReturnSameObject(t) && (asObject && !t.IsValueType())) + return null; #if !NETCORE if (ShallowObjectCloner.IsSafeVariant()) return DeepClonerExprGenerator.GenerateClonerInternal(t, asObject); else return DeepClonerMsilGenerator.GenerateClonerInternal(t, asObject); #else - return DeepClonerExprGenerator.GenerateClonerInternal(t, asObject); + return DeepClonerExprGenerator.GenerateClonerInternal(t, asObject); #endif - } + } - public static object CloneObjectTo(object objFrom, object objTo, bool isDeep) - { - if (objTo == null) return null; - - if (objFrom == null) - throw new ArgumentNullException("objFrom", "Cannot copy null object to another"); - var type = objFrom.GetType(); - if (!type.IsInstanceOfType(objTo)) - throw new InvalidOperationException("From object should be derived from From object, but From object has type " + objFrom.GetType().FullName + " and to " + objTo.GetType().FullName); - if (objFrom is string) - throw new InvalidOperationException("It is forbidden to clone strings"); - var cloner = (Func)(isDeep - ? DeepClonerCache.GetOrAddDeepClassTo(type, t => ClonerToExprGenerator.GenerateClonerInternal(t, true)) - : DeepClonerCache.GetOrAddShallowClassTo(type, t => ClonerToExprGenerator.GenerateClonerInternal(t, false))); - if (cloner == null) return objTo; - return cloner(objFrom, objTo, new DeepCloneState()); - } + public static object CloneObjectTo(object objFrom, object objTo, bool isDeep) + { + if (objTo == null) return null; + + if (objFrom == null) + throw new ArgumentNullException("objFrom", "Cannot copy null object to another"); + var type = objFrom.GetType(); + if (!type.IsInstanceOfType(objTo)) + throw new InvalidOperationException("From object should be derived from From object, but From object has type " + objFrom.GetType().FullName + " and to " + objTo.GetType().FullName); + if (objFrom is string) + throw new InvalidOperationException("It is forbidden to clone strings"); + var cloner = (Func)(isDeep + ? DeepClonerCache.GetOrAddDeepClassTo(type, t => ClonerToExprGenerator.GenerateClonerInternal(t, true)) + : DeepClonerCache.GetOrAddShallowClassTo(type, t => ClonerToExprGenerator.GenerateClonerInternal(t, false))); + if (cloner == null) return objTo; + return cloner(objFrom, objTo, new DeepCloneState()); } } diff --git a/src/Foundatio/DeepCloner/Helpers/DeepClonerSafeTypes.cs b/src/Foundatio/DeepCloner/Helpers/DeepClonerSafeTypes.cs index 2f1c55ba5..2d9dbabbe 100644 --- a/src/Foundatio/DeepCloner/Helpers/DeepClonerSafeTypes.cs +++ b/src/Foundatio/DeepCloner/Helpers/DeepClonerSafeTypes.cs @@ -5,46 +5,46 @@ using System.Linq; using System.Reflection; -namespace Foundatio.Force.DeepCloner.Helpers +namespace Foundatio.Force.DeepCloner.Helpers; + +/// +/// Safe types are types, which can be copied without real cloning. e.g. simple structs or strings (it is immutable) +/// +internal static class DeepClonerSafeTypes { - /// - /// Safe types are types, which can be copied without real cloning. e.g. simple structs or strings (it is immutable) - /// - internal static class DeepClonerSafeTypes - { - internal static readonly ConcurrentDictionary KnownTypes = new(); + internal static readonly ConcurrentDictionary KnownTypes = new(); - static DeepClonerSafeTypes() - { - foreach ( - var x in - new[] - { - typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), - typeof(float), typeof(double), typeof(decimal), typeof(char), typeof(string), typeof(bool), typeof(DateTime), - typeof(IntPtr), typeof(UIntPtr), typeof(Guid), - // do not clone such native type - Type.GetType("System.RuntimeType"), - Type.GetType("System.RuntimeTypeHandle"), + static DeepClonerSafeTypes() + { + foreach ( + var x in + new[] + { + typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), + typeof(float), typeof(double), typeof(decimal), typeof(char), typeof(string), typeof(bool), typeof(DateTime), + typeof(IntPtr), typeof(UIntPtr), typeof(Guid), + // do not clone such native type + Type.GetType("System.RuntimeType"), + Type.GetType("System.RuntimeTypeHandle"), #if !NETCORE typeof(DBNull) #endif - }) KnownTypes.TryAdd(x, true); - } + }) KnownTypes.TryAdd(x, true); + } - private static bool CanReturnSameType(Type type, HashSet processingTypes) - { - bool isSafe; - if (KnownTypes.TryGetValue(type, out isSafe)) - return isSafe; + private static bool CanReturnSameType(Type type, HashSet processingTypes) + { + bool isSafe; + if (KnownTypes.TryGetValue(type, out isSafe)) + return isSafe; - // enums are safe - // pointers (e.g. int*) are unsafe, but we cannot do anything with it except blind copy - if (type.IsEnum() || type.IsPointer) - { - KnownTypes.TryAdd(type, true); - return true; - } + // enums are safe + // pointers (e.g. int*) are unsafe, but we cannot do anything with it except blind copy + if (type.IsEnum() || type.IsPointer) + { + KnownTypes.TryAdd(type, true); + return true; + } #if !NETCORE // do not do anything with remoting. it is very dangerous to clone, bcs it relate to deep core of framework @@ -63,11 +63,11 @@ private static bool CanReturnSameType(Type type, HashSet processingTypes) // catched by previous condition /*if (type.FullName.StartsWith("System.Reflection.Emit") && type.Assembly == typeof(System.Reflection.Emit.OpCode).Assembly) - { - KnownTypes.TryAdd(type, true); - return true; - }*/ - + { + KnownTypes.TryAdd(type, true); + return true; + }*/ + // this types are serious native resources, it is better not to clone it if (type.IsSubclassOf(typeof(System.Runtime.ConstrainedExecution.CriticalFinalizerObject))) { @@ -82,118 +82,117 @@ private static bool CanReturnSameType(Type type, HashSet processingTypes) return true; } #else - // do not copy db null - if (type.FullName.StartsWith("System.DBNull")) - { - KnownTypes.TryAdd(type, true); - return true; - } + // do not copy db null + if (type.FullName.StartsWith("System.DBNull")) + { + KnownTypes.TryAdd(type, true); + return true; + } - if (type.FullName.StartsWith("System.RuntimeType")) - { - KnownTypes.TryAdd(type, true); - return true; - } + if (type.FullName.StartsWith("System.RuntimeType")) + { + KnownTypes.TryAdd(type, true); + return true; + } - if (type.FullName.StartsWith("System.Reflection.") && Equals(type.GetTypeInfo().Assembly, typeof(PropertyInfo).GetTypeInfo().Assembly)) - { - KnownTypes.TryAdd(type, true); - return true; - } + if (type.FullName.StartsWith("System.Reflection.") && Equals(type.GetTypeInfo().Assembly, typeof(PropertyInfo).GetTypeInfo().Assembly)) + { + KnownTypes.TryAdd(type, true); + return true; + } - if (type.IsSubclassOfTypeByName("CriticalFinalizerObject")) - { - KnownTypes.TryAdd(type, true); - return true; - } + if (type.IsSubclassOfTypeByName("CriticalFinalizerObject")) + { + KnownTypes.TryAdd(type, true); + return true; + } - // better not to touch ms dependency injection - if (type.FullName.StartsWith("Microsoft.Extensions.DependencyInjection.")) - { - KnownTypes.TryAdd(type, true); - return true; - } + // better not to touch ms dependency injection + if (type.FullName.StartsWith("Microsoft.Extensions.DependencyInjection.")) + { + KnownTypes.TryAdd(type, true); + return true; + } - if (type.FullName == "Microsoft.EntityFrameworkCore.Internal.ConcurrencyDetector") - { - KnownTypes.TryAdd(type, true); - return true; - } + if (type.FullName == "Microsoft.EntityFrameworkCore.Internal.ConcurrencyDetector") + { + KnownTypes.TryAdd(type, true); + return true; + } #endif - // classes are always unsafe (we should copy it fully to count references) - if (!type.IsValueType()) - { - KnownTypes.TryAdd(type, false); - return false; - } + // classes are always unsafe (we should copy it fully to count references) + if (!type.IsValueType()) + { + KnownTypes.TryAdd(type, false); + return false; + } - if (processingTypes == null) - processingTypes = new HashSet(); + if (processingTypes == null) + processingTypes = new HashSet(); - // structs cannot have a loops, but check it anyway - processingTypes.Add(type); + // structs cannot have a loops, but check it anyway + processingTypes.Add(type); - List fi = new List(); - var tp = type; - do - { - fi.AddRange(tp.GetAllFields()); - tp = tp.BaseType(); - } - while (tp != null); + List fi = new List(); + var tp = type; + do + { + fi.AddRange(tp.GetAllFields()); + tp = tp.BaseType(); + } + while (tp != null); + + foreach (var fieldInfo in fi) + { + // type loop + var fieldType = fieldInfo.FieldType; + if (processingTypes.Contains(fieldType)) + continue; - foreach (var fieldInfo in fi) + // not safe and not not safe. we need to go deeper + if (!CanReturnSameType(fieldType, processingTypes)) { - // type loop - var fieldType = fieldInfo.FieldType; - if (processingTypes.Contains(fieldType)) - continue; - - // not safe and not not safe. we need to go deeper - if (!CanReturnSameType(fieldType, processingTypes)) - { - KnownTypes.TryAdd(type, false); - return false; - } + KnownTypes.TryAdd(type, false); + return false; } - - KnownTypes.TryAdd(type, true); - return true; } - // not used anymore - /*/// - /// Classes with only safe fields are safe for ShallowClone (if they root objects for copying) - /// - private static bool CanCopyClassInShallow(Type type) - { - // do not do this anything for struct and arrays - if (!type.IsClass() || type.IsArray) - { - return false; - } - - List fi = new List(); - var tp = type; - do - { - fi.AddRange(tp.GetAllFields()); - tp = tp.BaseType(); - } - while (tp != null); + KnownTypes.TryAdd(type, true); + return true; + } - if (fi.Any(fieldInfo => !CanReturnSameType(fieldInfo.FieldType, null))) - { - return false; - } + // not used anymore + /*/// + /// Classes with only safe fields are safe for ShallowClone (if they root objects for copying) + /// + private static bool CanCopyClassInShallow(Type type) + { + // do not do this anything for struct and arrays + if (!type.IsClass() || type.IsArray) + { + return false; + } - return true; - }*/ + List fi = new List(); + var tp = type; + do + { + fi.AddRange(tp.GetAllFields()); + tp = tp.BaseType(); + } + while (tp != null); - public static bool CanReturnSameObject(Type type) + if (fi.Any(fieldInfo => !CanReturnSameType(fieldInfo.FieldType, null))) { - return CanReturnSameType(type, null); + return false; } + + return true; + }*/ + + public static bool CanReturnSameObject(Type type) + { + return CanReturnSameType(type, null); } } diff --git a/src/Foundatio/DeepCloner/Helpers/ReflectionHelper.cs b/src/Foundatio/DeepCloner/Helpers/ReflectionHelper.cs index 0aec3542e..aaafb1f00 100644 --- a/src/Foundatio/DeepCloner/Helpers/ReflectionHelper.cs +++ b/src/Foundatio/DeepCloner/Helpers/ReflectionHelper.cs @@ -3,169 +3,168 @@ using System.Linq; using System.Reflection; -namespace Foundatio.Force.DeepCloner.Helpers +namespace Foundatio.Force.DeepCloner.Helpers; + +internal static class ReflectionHelper { - internal static class ReflectionHelper + public static bool IsEnum(this Type t) { - public static bool IsEnum(this Type t) - { #if NETCORE - return t.GetTypeInfo().IsEnum; + return t.GetTypeInfo().IsEnum; #else return t.IsEnum; #endif - } + } - public static bool IsValueType(this Type t) - { + public static bool IsValueType(this Type t) + { #if NETCORE - return t.GetTypeInfo().IsValueType; + return t.GetTypeInfo().IsValueType; #else return t.IsValueType; #endif - } + } - public static bool IsClass(this Type t) - { + public static bool IsClass(this Type t) + { #if NETCORE - return t.GetTypeInfo().IsClass; + return t.GetTypeInfo().IsClass; #else return t.IsClass; #endif - } + } - public static Type BaseType(this Type t) - { + public static Type BaseType(this Type t) + { #if NETCORE - return t.GetTypeInfo().BaseType; + return t.GetTypeInfo().BaseType; #else return t.BaseType; #endif - } + } - public static FieldInfo[] GetAllFields(this Type t) - { + public static FieldInfo[] GetAllFields(this Type t) + { #if NETCORE - return t.GetTypeInfo().DeclaredFields.Where(x => !x.IsStatic).ToArray(); + return t.GetTypeInfo().DeclaredFields.Where(x => !x.IsStatic).ToArray(); #else return t.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); #endif - } + } - public static PropertyInfo[] GetPublicProperties(this Type t) - { + public static PropertyInfo[] GetPublicProperties(this Type t) + { #if NETCORE - return t.GetTypeInfo().DeclaredProperties.ToArray(); + return t.GetTypeInfo().DeclaredProperties.ToArray(); #else return t.GetProperties(BindingFlags.Instance | BindingFlags.Public); #endif - } + } - public static FieldInfo[] GetDeclaredFields(this Type t) - { + public static FieldInfo[] GetDeclaredFields(this Type t) + { #if NETCORE - return t.GetTypeInfo().DeclaredFields.Where(x => !x.IsStatic).ToArray(); + return t.GetTypeInfo().DeclaredFields.Where(x => !x.IsStatic).ToArray(); #else return t.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.DeclaredOnly); #endif - } + } - public static ConstructorInfo[] GetPrivateConstructors(this Type t) - { + public static ConstructorInfo[] GetPrivateConstructors(this Type t) + { #if NETCORE - return t.GetTypeInfo().DeclaredConstructors.ToArray(); + return t.GetTypeInfo().DeclaredConstructors.ToArray(); #else return t.GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance); #endif - } + } - public static ConstructorInfo[] GetPublicConstructors(this Type t) - { + public static ConstructorInfo[] GetPublicConstructors(this Type t) + { #if NETCORE - return t.GetTypeInfo().DeclaredConstructors.ToArray(); + return t.GetTypeInfo().DeclaredConstructors.ToArray(); #else return t.GetConstructors(BindingFlags.Public | BindingFlags.Instance); #endif - } + } - public static MethodInfo GetPrivateMethod(this Type t, string methodName) - { + public static MethodInfo GetPrivateMethod(this Type t, string methodName) + { #if NETCORE - return t.GetTypeInfo().GetDeclaredMethod(methodName); + return t.GetTypeInfo().GetDeclaredMethod(methodName); #else return t.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance); #endif - } + } - public static MethodInfo GetMethod(this Type t, string methodName) - { + public static MethodInfo GetMethod(this Type t, string methodName) + { #if NETCORE - return t.GetTypeInfo().GetDeclaredMethod(methodName); + return t.GetTypeInfo().GetDeclaredMethod(methodName); #else return t.GetMethod(methodName); #endif - } + } - public static MethodInfo GetPrivateStaticMethod(this Type t, string methodName) - { + public static MethodInfo GetPrivateStaticMethod(this Type t, string methodName) + { #if NETCORE - return t.GetTypeInfo().GetDeclaredMethod(methodName); + return t.GetTypeInfo().GetDeclaredMethod(methodName); #else return t.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Static); #endif - } + } - public static FieldInfo GetPrivateField(this Type t, string fieldName) - { + public static FieldInfo GetPrivateField(this Type t, string fieldName) + { #if NETCORE - return t.GetTypeInfo().GetDeclaredField(fieldName); + return t.GetTypeInfo().GetDeclaredField(fieldName); #else return t.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance); #endif - } + } - public static FieldInfo GetPrivateStaticField(this Type t, string fieldName) - { + public static FieldInfo GetPrivateStaticField(this Type t, string fieldName) + { #if NETCORE - return t.GetTypeInfo().GetDeclaredField(fieldName); + return t.GetTypeInfo().GetDeclaredField(fieldName); #else return t.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Static); #endif - } + } #if NETCORE - public static bool IsSubclassOfTypeByName(this Type t, string typeName) + public static bool IsSubclassOfTypeByName(this Type t, string typeName) + { + while (t != null) { - while (t != null) - { - if (t.Name == typeName) - return true; - t = t.BaseType(); - } - - return false; + if (t.Name == typeName) + return true; + t = t.BaseType(); } + + return false; + } #endif #if NETCORE - public static bool IsAssignableFrom(this Type from, Type to) - { - return from.GetTypeInfo().IsAssignableFrom(to.GetTypeInfo()); - } + public static bool IsAssignableFrom(this Type from, Type to) + { + return from.GetTypeInfo().IsAssignableFrom(to.GetTypeInfo()); + } - public static bool IsInstanceOfType(this Type from, object to) - { - return from.IsAssignableFrom(to.GetType()); - } + public static bool IsInstanceOfType(this Type from, object to) + { + return from.IsAssignableFrom(to.GetType()); + } #endif - public static Type[] GenericArguments(this Type t) - { + public static Type[] GenericArguments(this Type t) + { #if NETCORE - return t.GetTypeInfo().GenericTypeArguments; + return t.GetTypeInfo().GenericTypeArguments; #else return t.GetGenericArguments(); #endif - } } } diff --git a/src/Foundatio/DeepCloner/Helpers/ShallowClonerGenerator.cs b/src/Foundatio/DeepCloner/Helpers/ShallowClonerGenerator.cs index 2cd2b70ee..ed88c3912 100644 --- a/src/Foundatio/DeepCloner/Helpers/ShallowClonerGenerator.cs +++ b/src/Foundatio/DeepCloner/Helpers/ShallowClonerGenerator.cs @@ -1,29 +1,28 @@ using System; -namespace Foundatio.Force.DeepCloner.Helpers +namespace Foundatio.Force.DeepCloner.Helpers; + +internal static class ShallowClonerGenerator { - internal static class ShallowClonerGenerator + public static T CloneObject(T obj) { - public static T CloneObject(T obj) + // this is faster than typeof(T).IsValueType + if (obj is ValueType) { - // this is faster than typeof(T).IsValueType - if (obj is ValueType) - { - if (typeof(T) == obj.GetType()) - return obj; - - // we're here so, we clone value type obj as object type T - // so, we need to copy it, bcs we have a reference, not real object. - return (T)ShallowObjectCloner.CloneObject(obj); - } - - if (ReferenceEquals(obj, null)) - return (T)(object)null; - - if (DeepClonerSafeTypes.CanReturnSameObject(obj.GetType())) + if (typeof(T) == obj.GetType()) return obj; + // we're here so, we clone value type obj as object type T + // so, we need to copy it, bcs we have a reference, not real object. return (T)ShallowObjectCloner.CloneObject(obj); } + + if (ReferenceEquals(obj, null)) + return (T)(object)null; + + if (DeepClonerSafeTypes.CanReturnSameObject(obj.GetType())) + return obj; + + return (T)ShallowObjectCloner.CloneObject(obj); } } diff --git a/src/Foundatio/DeepCloner/Helpers/ShallowObjectCloner.cs b/src/Foundatio/DeepCloner/Helpers/ShallowObjectCloner.cs index db1f81fb7..78ff49ffd 100644 --- a/src/Foundatio/DeepCloner/Helpers/ShallowObjectCloner.cs +++ b/src/Foundatio/DeepCloner/Helpers/ShallowObjectCloner.cs @@ -4,37 +4,37 @@ using System.Reflection; using System.Reflection.Emit; -namespace Foundatio.Force.DeepCloner.Helpers +namespace Foundatio.Force.DeepCloner.Helpers; + +/// +/// Internal class but due implementation restriction should be public +/// +internal abstract class ShallowObjectCloner { /// - /// Internal class but due implementation restriction should be public + /// Abstract method for real object cloning /// - internal abstract class ShallowObjectCloner - { - /// - /// Abstract method for real object cloning - /// - protected abstract object DoCloneObject(object obj); + protected abstract object DoCloneObject(object obj); - private static readonly ShallowObjectCloner _unsafeInstance; + private static readonly ShallowObjectCloner _unsafeInstance; - private static ShallowObjectCloner _instance; + private static ShallowObjectCloner _instance; - /// - /// Performs real shallow object clone - /// - public static object CloneObject(object obj) - { - return _instance.DoCloneObject(obj); - } + /// + /// Performs real shallow object clone + /// + public static object CloneObject(object obj) + { + return _instance.DoCloneObject(obj); + } - internal static bool IsSafeVariant() - { - return _instance is ShallowSafeObjectCloner; - } + internal static bool IsSafeVariant() + { + return _instance is ShallowSafeObjectCloner; + } - static ShallowObjectCloner() - { + static ShallowObjectCloner() + { #if !NETCORE _unsafeInstance = GenerateUnsafeCloner(); _instance = _unsafeInstance; @@ -48,21 +48,21 @@ static ShallowObjectCloner() _instance = new ShallowSafeObjectCloner(); } #else - _instance = new ShallowSafeObjectCloner(); - // no unsafe variant for core - _unsafeInstance = _instance; + _instance = new ShallowSafeObjectCloner(); + // no unsafe variant for core + _unsafeInstance = _instance; #endif - } + } - /// - /// Purpose of this method is testing variants - /// - internal static void SwitchTo(bool isSafe) - { - DeepClonerCache.ClearCache(); - if (isSafe) _instance = new ShallowSafeObjectCloner(); - else _instance = _unsafeInstance; - } + /// + /// Purpose of this method is testing variants + /// + internal static void SwitchTo(bool isSafe) + { + DeepClonerCache.ClearCache(); + if (isSafe) _instance = new ShallowSafeObjectCloner(); + else _instance = _unsafeInstance; + } #if !NETCORE private static ShallowObjectCloner GenerateUnsafeCloner() @@ -95,22 +95,21 @@ private static ShallowObjectCloner GenerateUnsafeCloner() } #endif - private class ShallowSafeObjectCloner : ShallowObjectCloner + private class ShallowSafeObjectCloner : ShallowObjectCloner + { + private static readonly Func _cloneFunc; + + static ShallowSafeObjectCloner() + { + var methodInfo = typeof(object).GetPrivateMethod("MemberwiseClone"); + var p = Expression.Parameter(typeof(object)); + var mce = Expression.Call(p, methodInfo); + _cloneFunc = Expression.Lambda>(mce, p).Compile(); + } + + protected override object DoCloneObject(object obj) { - private static readonly Func _cloneFunc; - - static ShallowSafeObjectCloner() - { - var methodInfo = typeof(object).GetPrivateMethod("MemberwiseClone"); - var p = Expression.Parameter(typeof(object)); - var mce = Expression.Call(p, methodInfo); - _cloneFunc = Expression.Lambda>(mce, p).Compile(); - } - - protected override object DoCloneObject(object obj) - { - return _cloneFunc(obj); - } + return _cloneFunc(obj); } } } diff --git a/src/Foundatio/Extensions/CacheClientExtensions.cs b/src/Foundatio/Extensions/CacheClientExtensions.cs index b92304c94..76341a0f7 100644 --- a/src/Foundatio/Extensions/CacheClientExtensions.cs +++ b/src/Foundatio/Extensions/CacheClientExtensions.cs @@ -4,174 +4,173 @@ using System.Threading.Tasks; using Foundatio.Utility; -namespace Foundatio.Caching +namespace Foundatio.Caching; + +public static class CacheClientExtensions { - public static class CacheClientExtensions - { - public static async Task GetAsync(this ICacheClient client, string key, T defaultValue) - { - var cacheValue = await client.GetAsync(key).AnyContext(); - return cacheValue.HasValue ? cacheValue.Value : defaultValue; - } - - public static Task>> GetAllAsync(this ICacheClient client, params string[] keys) - { - return client.GetAllAsync(keys.ToArray()); - } - - public static Task IncrementAsync(this ICacheClient client, string key, long amount, DateTime? expiresAtUtc) - { - return client.IncrementAsync(key, amount, expiresAtUtc?.Subtract(SystemClock.UtcNow)); - } - - public static Task IncrementAsync(this ICacheClient client, string key, double amount, DateTime? expiresAtUtc) - { - return client.IncrementAsync(key, amount, expiresAtUtc?.Subtract(SystemClock.UtcNow)); - } - - public static Task IncrementAsync(this ICacheClient client, string key, TimeSpan? expiresIn = null) - { - return client.IncrementAsync(key, 1, expiresIn); - } - - public static Task DecrementAsync(this ICacheClient client, string key, TimeSpan? expiresIn = null) - { - return client.IncrementAsync(key, -1, expiresIn); - } - - public static Task DecrementAsync(this ICacheClient client, string key, long amount, TimeSpan? expiresIn = null) - { - return client.IncrementAsync(key, -amount, expiresIn); - } - - public static Task DecrementAsync(this ICacheClient client, string key, long amount, DateTime? expiresAtUtc) - { - return client.IncrementAsync(key, -amount, expiresAtUtc?.Subtract(SystemClock.UtcNow)); - } - - public static Task DecrementAsync(this ICacheClient client, string key, double amount, DateTime? expiresAtUtc) - { - return client.IncrementAsync(key, -amount, expiresAtUtc?.Subtract(SystemClock.UtcNow)); - } - - public static Task AddAsync(this ICacheClient client, string key, T value, DateTime? expiresAtUtc) - { - return client.AddAsync(key, value, expiresAtUtc?.Subtract(SystemClock.UtcNow)); - } - - public static Task SetAsync(this ICacheClient client, string key, T value, DateTime? expiresAtUtc) - { - return client.SetAsync(key, value, expiresAtUtc?.Subtract(SystemClock.UtcNow)); - } - - public static Task ReplaceAsync(this ICacheClient client, string key, T value, DateTime? expiresAtUtc) - { - return client.ReplaceAsync(key, value, expiresAtUtc?.Subtract(SystemClock.UtcNow)); - } - - public static Task ReplaceIfEqualAsync(this ICacheClient client, string key, T value, T expected, DateTime? expiresAtUtc) - { - return client.ReplaceIfEqualAsync(key, value, expected, expiresAtUtc?.Subtract(SystemClock.UtcNow)); - } - - public static Task SetAllAsync(this ICacheClient client, IDictionary values, DateTime? expiresAtUtc) - { - return client.SetAllAsync(values, expiresAtUtc?.Subtract(SystemClock.UtcNow)); - } - - public static Task SetExpirationAsync(this ICacheClient client, string key, DateTime expiresAtUtc) - { - return client.SetExpirationAsync(key, expiresAtUtc.Subtract(SystemClock.UtcNow)); - } - - public static async Task ListAddAsync(this ICacheClient client, string key, T value, TimeSpan? expiresIn = null) - { - return await client.ListAddAsync(key, new[] { value }, expiresIn).AnyContext() > 0; - } - - public static async Task ListRemoveAsync(this ICacheClient client, string key, T value, TimeSpan? expiresIn = null) - { - return await client.ListRemoveAsync(key, new[] { value }, expiresIn).AnyContext() > 0; - } - - [Obsolete("Use ListAddAsync instead")] - public static async Task SetAddAsync(this ICacheClient client, string key, T value, TimeSpan? expiresIn = null) - { - return await client.ListAddAsync(key, new[] { value }, expiresIn).AnyContext() > 0; - } - - [Obsolete("Use ListRemoveAsync instead")] - public static async Task SetRemoveAsync(this ICacheClient client, string key, T value, TimeSpan? expiresIn = null) - { - return await client.ListRemoveAsync(key, new[] { value }, expiresIn).AnyContext() > 0; - } - - [Obsolete("Use ListAddAsync instead")] - public static Task SetAddAsync(this ICacheClient client, string key, IEnumerable value, TimeSpan? expiresIn = null) - { - return client.ListAddAsync(key, new[] { value }, expiresIn); - } - - [Obsolete("Use ListRemoveAsync instead")] - public static Task SetRemoveAsync(this ICacheClient client, string key, IEnumerable value, TimeSpan? expiresIn = null) - { - return client.ListRemoveAsync(key, value, expiresIn); - } - - [Obsolete("Use ListAddAsync instead")] - public static Task>> GetSetAsync(this ICacheClient client, string key) - { - return client.GetListAsync(key); - } - - public static Task SetIfHigherAsync(this ICacheClient client, string key, DateTime value, TimeSpan? expiresIn = null) - { - long unixTime = value.ToUnixTimeMilliseconds(); - return client.SetIfHigherAsync(key, unixTime, expiresIn); - } - - public static Task SetIfLowerAsync(this ICacheClient client, string key, DateTime value, TimeSpan? expiresIn = null) - { - long unixTime = value.ToUnixTimeMilliseconds(); - return client.SetIfLowerAsync(key, unixTime, expiresIn); - } - - public static async Task GetUnixTimeMillisecondsAsync(this ICacheClient client, string key, DateTime? defaultValue = null) - { - var unixTime = await client.GetAsync(key).AnyContext(); - if (!unixTime.HasValue) - return defaultValue ?? DateTime.MinValue; - - return unixTime.Value.FromUnixTimeMilliseconds(); - } - - public static Task SetUnixTimeMillisecondsAsync(this ICacheClient client, string key, DateTime value, TimeSpan? expiresIn = null) - { - return client.SetAsync(key, value.ToUnixTimeMilliseconds(), expiresIn); - } - - public static Task SetUnixTimeMillisecondsAsync(this ICacheClient client, string key, DateTime value, DateTime? expiresAtUtc) - { - return client.SetAsync(key, value.ToUnixTimeMilliseconds(), expiresAtUtc?.Subtract(SystemClock.UtcNow)); - } - - public static async Task GetUnixTimeSecondsAsync(this ICacheClient client, string key, DateTime? defaultValue = null) - { - var unixTime = await client.GetAsync(key).AnyContext(); - if (!unixTime.HasValue) - return defaultValue ?? DateTime.MinValue; - - return unixTime.Value.FromUnixTimeSeconds(); - } - - public static Task SetUnixTimeSecondsAsync(this ICacheClient client, string key, DateTime value, TimeSpan? expiresIn = null) - { - return client.SetAsync(key, value.ToUnixTimeSeconds(), expiresIn); - } - - public static Task SetUnixTimeSecondsAsync(this ICacheClient client, string key, DateTime value, DateTime? expiresAtUtc) - { - return client.SetAsync(key, value.ToUnixTimeSeconds(), expiresAtUtc?.Subtract(SystemClock.UtcNow)); - } + public static async Task GetAsync(this ICacheClient client, string key, T defaultValue) + { + var cacheValue = await client.GetAsync(key).AnyContext(); + return cacheValue.HasValue ? cacheValue.Value : defaultValue; + } + + public static Task>> GetAllAsync(this ICacheClient client, params string[] keys) + { + return client.GetAllAsync(keys.ToArray()); + } + + public static Task IncrementAsync(this ICacheClient client, string key, long amount, DateTime? expiresAtUtc) + { + return client.IncrementAsync(key, amount, expiresAtUtc?.Subtract(SystemClock.UtcNow)); + } + + public static Task IncrementAsync(this ICacheClient client, string key, double amount, DateTime? expiresAtUtc) + { + return client.IncrementAsync(key, amount, expiresAtUtc?.Subtract(SystemClock.UtcNow)); + } + + public static Task IncrementAsync(this ICacheClient client, string key, TimeSpan? expiresIn = null) + { + return client.IncrementAsync(key, 1, expiresIn); + } + + public static Task DecrementAsync(this ICacheClient client, string key, TimeSpan? expiresIn = null) + { + return client.IncrementAsync(key, -1, expiresIn); + } + + public static Task DecrementAsync(this ICacheClient client, string key, long amount, TimeSpan? expiresIn = null) + { + return client.IncrementAsync(key, -amount, expiresIn); + } + + public static Task DecrementAsync(this ICacheClient client, string key, long amount, DateTime? expiresAtUtc) + { + return client.IncrementAsync(key, -amount, expiresAtUtc?.Subtract(SystemClock.UtcNow)); + } + + public static Task DecrementAsync(this ICacheClient client, string key, double amount, DateTime? expiresAtUtc) + { + return client.IncrementAsync(key, -amount, expiresAtUtc?.Subtract(SystemClock.UtcNow)); + } + + public static Task AddAsync(this ICacheClient client, string key, T value, DateTime? expiresAtUtc) + { + return client.AddAsync(key, value, expiresAtUtc?.Subtract(SystemClock.UtcNow)); + } + + public static Task SetAsync(this ICacheClient client, string key, T value, DateTime? expiresAtUtc) + { + return client.SetAsync(key, value, expiresAtUtc?.Subtract(SystemClock.UtcNow)); + } + + public static Task ReplaceAsync(this ICacheClient client, string key, T value, DateTime? expiresAtUtc) + { + return client.ReplaceAsync(key, value, expiresAtUtc?.Subtract(SystemClock.UtcNow)); + } + + public static Task ReplaceIfEqualAsync(this ICacheClient client, string key, T value, T expected, DateTime? expiresAtUtc) + { + return client.ReplaceIfEqualAsync(key, value, expected, expiresAtUtc?.Subtract(SystemClock.UtcNow)); + } + + public static Task SetAllAsync(this ICacheClient client, IDictionary values, DateTime? expiresAtUtc) + { + return client.SetAllAsync(values, expiresAtUtc?.Subtract(SystemClock.UtcNow)); + } + + public static Task SetExpirationAsync(this ICacheClient client, string key, DateTime expiresAtUtc) + { + return client.SetExpirationAsync(key, expiresAtUtc.Subtract(SystemClock.UtcNow)); + } + + public static async Task ListAddAsync(this ICacheClient client, string key, T value, TimeSpan? expiresIn = null) + { + return await client.ListAddAsync(key, new[] { value }, expiresIn).AnyContext() > 0; + } + + public static async Task ListRemoveAsync(this ICacheClient client, string key, T value, TimeSpan? expiresIn = null) + { + return await client.ListRemoveAsync(key, new[] { value }, expiresIn).AnyContext() > 0; + } + + [Obsolete("Use ListAddAsync instead")] + public static async Task SetAddAsync(this ICacheClient client, string key, T value, TimeSpan? expiresIn = null) + { + return await client.ListAddAsync(key, new[] { value }, expiresIn).AnyContext() > 0; + } + + [Obsolete("Use ListRemoveAsync instead")] + public static async Task SetRemoveAsync(this ICacheClient client, string key, T value, TimeSpan? expiresIn = null) + { + return await client.ListRemoveAsync(key, new[] { value }, expiresIn).AnyContext() > 0; + } + + [Obsolete("Use ListAddAsync instead")] + public static Task SetAddAsync(this ICacheClient client, string key, IEnumerable value, TimeSpan? expiresIn = null) + { + return client.ListAddAsync(key, new[] { value }, expiresIn); + } + + [Obsolete("Use ListRemoveAsync instead")] + public static Task SetRemoveAsync(this ICacheClient client, string key, IEnumerable value, TimeSpan? expiresIn = null) + { + return client.ListRemoveAsync(key, value, expiresIn); + } + + [Obsolete("Use ListAddAsync instead")] + public static Task>> GetSetAsync(this ICacheClient client, string key) + { + return client.GetListAsync(key); + } + + public static Task SetIfHigherAsync(this ICacheClient client, string key, DateTime value, TimeSpan? expiresIn = null) + { + long unixTime = value.ToUnixTimeMilliseconds(); + return client.SetIfHigherAsync(key, unixTime, expiresIn); + } + + public static Task SetIfLowerAsync(this ICacheClient client, string key, DateTime value, TimeSpan? expiresIn = null) + { + long unixTime = value.ToUnixTimeMilliseconds(); + return client.SetIfLowerAsync(key, unixTime, expiresIn); + } + + public static async Task GetUnixTimeMillisecondsAsync(this ICacheClient client, string key, DateTime? defaultValue = null) + { + var unixTime = await client.GetAsync(key).AnyContext(); + if (!unixTime.HasValue) + return defaultValue ?? DateTime.MinValue; + + return unixTime.Value.FromUnixTimeMilliseconds(); + } + + public static Task SetUnixTimeMillisecondsAsync(this ICacheClient client, string key, DateTime value, TimeSpan? expiresIn = null) + { + return client.SetAsync(key, value.ToUnixTimeMilliseconds(), expiresIn); + } + + public static Task SetUnixTimeMillisecondsAsync(this ICacheClient client, string key, DateTime value, DateTime? expiresAtUtc) + { + return client.SetAsync(key, value.ToUnixTimeMilliseconds(), expiresAtUtc?.Subtract(SystemClock.UtcNow)); + } + + public static async Task GetUnixTimeSecondsAsync(this ICacheClient client, string key, DateTime? defaultValue = null) + { + var unixTime = await client.GetAsync(key).AnyContext(); + if (!unixTime.HasValue) + return defaultValue ?? DateTime.MinValue; + + return unixTime.Value.FromUnixTimeSeconds(); + } + + public static Task SetUnixTimeSecondsAsync(this ICacheClient client, string key, DateTime value, TimeSpan? expiresIn = null) + { + return client.SetAsync(key, value.ToUnixTimeSeconds(), expiresIn); + } + + public static Task SetUnixTimeSecondsAsync(this ICacheClient client, string key, DateTime value, DateTime? expiresAtUtc) + { + return client.SetAsync(key, value.ToUnixTimeSeconds(), expiresAtUtc?.Subtract(SystemClock.UtcNow)); } } diff --git a/src/Foundatio/Extensions/CollectionExtensions.cs b/src/Foundatio/Extensions/CollectionExtensions.cs index 377098631..740eb4087 100644 --- a/src/Foundatio/Extensions/CollectionExtensions.cs +++ b/src/Foundatio/Extensions/CollectionExtensions.cs @@ -2,30 +2,29 @@ using System.Collections.Generic; using System.Linq; -namespace Foundatio.Utility +namespace Foundatio.Utility; + +internal static class CollectionExtensions { - internal static class CollectionExtensions + public static ICollection ReduceTimeSeries(this ICollection items, Func dateSelector, Func, DateTime, T> reducer, int dataPoints) { - public static ICollection ReduceTimeSeries(this ICollection items, Func dateSelector, Func, DateTime, T> reducer, int dataPoints) - { - if (items.Count <= dataPoints) - return items; + if (items.Count <= dataPoints) + return items; - var minTicks = items.Min(dateSelector).Ticks; - var maxTicks = items.Max(dateSelector).Ticks; + var minTicks = items.Min(dateSelector).Ticks; + var maxTicks = items.Max(dateSelector).Ticks; - var bucketSize = (maxTicks - minTicks) / dataPoints; - var buckets = new List(); - long currentTick = minTicks; - while (currentTick < maxTicks) - { - buckets.Add(currentTick); - currentTick += bucketSize; - } + var bucketSize = (maxTicks - minTicks) / dataPoints; + var buckets = new List(); + long currentTick = minTicks; + while (currentTick < maxTicks) + { + buckets.Add(currentTick); + currentTick += bucketSize; + } - buckets.Reverse(); + buckets.Reverse(); - return items.GroupBy(i => buckets.First(b => dateSelector(i).Ticks >= b)).Select(g => reducer(g.ToList(), new DateTime(g.Key))).ToList(); - } + return items.GroupBy(i => buckets.First(b => dateSelector(i).Ticks >= b)).Select(g => reducer(g.ToList(), new DateTime(g.Key))).ToList(); } } diff --git a/src/Foundatio/Extensions/ConcurrentDictionaryExtensions.cs b/src/Foundatio/Extensions/ConcurrentDictionaryExtensions.cs index 92c3128c4..61c1b3456 100644 --- a/src/Foundatio/Extensions/ConcurrentDictionaryExtensions.cs +++ b/src/Foundatio/Extensions/ConcurrentDictionaryExtensions.cs @@ -1,29 +1,28 @@ using System; using System.Collections.Concurrent; -namespace Foundatio.Utility +namespace Foundatio.Utility; + +internal static class ConcurrentDictionaryExtensions { - internal static class ConcurrentDictionaryExtensions + public static bool TryUpdate(this ConcurrentDictionary concurrentDictionary, TKey key, Func updateValueFactory) { - public static bool TryUpdate(this ConcurrentDictionary concurrentDictionary, TKey key, Func updateValueFactory) - { - if (key == null) - throw new ArgumentNullException(nameof(key)); + if (key == null) + throw new ArgumentNullException(nameof(key)); - if (updateValueFactory == null) - throw new ArgumentNullException(nameof(updateValueFactory)); + if (updateValueFactory == null) + throw new ArgumentNullException(nameof(updateValueFactory)); - TValue comparisonValue; - TValue newValue; - do - { - if (!concurrentDictionary.TryGetValue(key, out comparisonValue)) - return false; + TValue comparisonValue; + TValue newValue; + do + { + if (!concurrentDictionary.TryGetValue(key, out comparisonValue)) + return false; - newValue = updateValueFactory(key, comparisonValue); - } while (!concurrentDictionary.TryUpdate(key, newValue, comparisonValue)); + newValue = updateValueFactory(key, comparisonValue); + } while (!concurrentDictionary.TryUpdate(key, newValue, comparisonValue)); - return true; - } + return true; } } diff --git a/src/Foundatio/Extensions/ConcurrentQueueExtensions.cs b/src/Foundatio/Extensions/ConcurrentQueueExtensions.cs index 9d005f413..45d961c47 100644 --- a/src/Foundatio/Extensions/ConcurrentQueueExtensions.cs +++ b/src/Foundatio/Extensions/ConcurrentQueueExtensions.cs @@ -1,12 +1,11 @@ using System.Collections.Concurrent; -namespace Foundatio.Utility +namespace Foundatio.Utility; + +internal static class ConcurrentQueueExtensions { - internal static class ConcurrentQueueExtensions + public static void Clear(this ConcurrentQueue queue) { - public static void Clear(this ConcurrentQueue queue) - { - while (queue.TryDequeue(out var _)) { } - } + while (queue.TryDequeue(out var _)) { } } } diff --git a/src/Foundatio/Extensions/DateTimeExtensions.cs b/src/Foundatio/Extensions/DateTimeExtensions.cs index ca3ba8331..37a5090db 100644 --- a/src/Foundatio/Extensions/DateTimeExtensions.cs +++ b/src/Foundatio/Extensions/DateTimeExtensions.cs @@ -1,48 +1,47 @@ using System; -namespace Foundatio.Utility +namespace Foundatio.Utility; + +internal static class DateTimeExtensions { - internal static class DateTimeExtensions + public static DateTime Floor(this DateTime date, TimeSpan interval) + { + return date.AddTicks(-(date.Ticks % interval.Ticks)); + } + + public static DateTime Ceiling(this DateTime date, TimeSpan interval) { - public static DateTime Floor(this DateTime date, TimeSpan interval) - { - return date.AddTicks(-(date.Ticks % interval.Ticks)); - } - - public static DateTime Ceiling(this DateTime date, TimeSpan interval) - { - return date.AddTicks(interval.Ticks - (date.Ticks % interval.Ticks)); - } - - public static long ToUnixTimeMilliseconds(this DateTime date) - { - return new DateTimeOffset(date.ToUniversalTime()).ToUnixTimeMilliseconds(); - } - - public static DateTime FromUnixTimeMilliseconds(this long timestamp) - { - return DateTimeOffset.FromUnixTimeMilliseconds(timestamp).UtcDateTime; - } - - public static long ToUnixTimeSeconds(this DateTime date) - { - return new DateTimeOffset(date.ToUniversalTime()).ToUnixTimeSeconds(); - } - - public static DateTime FromUnixTimeSeconds(this long timestamp) - { - return DateTimeOffset.FromUnixTimeSeconds(timestamp).UtcDateTime; - } - - public static DateTime SafeAdd(this DateTime date, TimeSpan value) - { - if (date.Ticks + value.Ticks < DateTime.MinValue.Ticks) - return DateTime.MinValue; - - if (date.Ticks + value.Ticks > DateTime.MaxValue.Ticks) - return DateTime.MaxValue; - - return date.Add(value); - } + return date.AddTicks(interval.Ticks - (date.Ticks % interval.Ticks)); + } + + public static long ToUnixTimeMilliseconds(this DateTime date) + { + return new DateTimeOffset(date.ToUniversalTime()).ToUnixTimeMilliseconds(); + } + + public static DateTime FromUnixTimeMilliseconds(this long timestamp) + { + return DateTimeOffset.FromUnixTimeMilliseconds(timestamp).UtcDateTime; + } + + public static long ToUnixTimeSeconds(this DateTime date) + { + return new DateTimeOffset(date.ToUniversalTime()).ToUnixTimeSeconds(); + } + + public static DateTime FromUnixTimeSeconds(this long timestamp) + { + return DateTimeOffset.FromUnixTimeSeconds(timestamp).UtcDateTime; + } + + public static DateTime SafeAdd(this DateTime date, TimeSpan value) + { + if (date.Ticks + value.Ticks < DateTime.MinValue.Ticks) + return DateTime.MinValue; + + if (date.Ticks + value.Ticks > DateTime.MaxValue.Ticks) + return DateTime.MaxValue; + + return date.Add(value); } } diff --git a/src/Foundatio/Extensions/EnumExtensions.cs b/src/Foundatio/Extensions/EnumExtensions.cs index 7b4e0ef63..853200e5b 100644 --- a/src/Foundatio/Extensions/EnumExtensions.cs +++ b/src/Foundatio/Extensions/EnumExtensions.cs @@ -1,58 +1,57 @@ using System; using System.Reflection; -namespace Foundatio.Utility +namespace Foundatio.Utility; + +internal static class EnumExtensions { - internal static class EnumExtensions + /// + /// Will try and parse an enum and it's default type. + /// + /// + /// + /// True if the enum value is defined. + public static bool TryEnumIsDefined(Type type, object value) { - /// - /// Will try and parse an enum and it's default type. - /// - /// - /// - /// True if the enum value is defined. - public static bool TryEnumIsDefined(Type type, object value) - { - if (type == null || value == null || !type.GetTypeInfo().IsEnum) - return false; + if (type == null || value == null || !type.GetTypeInfo().IsEnum) + return false; - // Return true if the value is an enum and is a matching type. - if (type == value.GetType()) - return true; + // Return true if the value is an enum and is a matching type. + if (type == value.GetType()) + return true; - if (TryEnumIsDefined(type, value)) - return true; - if (TryEnumIsDefined(type, value)) - return true; - if (TryEnumIsDefined(type, value)) - return true; - if (TryEnumIsDefined(type, value)) - return true; - if (TryEnumIsDefined(type, value)) - return true; - if (TryEnumIsDefined(type, value)) - return true; - if (TryEnumIsDefined(type, value)) - return true; - if (TryEnumIsDefined(type, value)) - return true; - if (TryEnumIsDefined(type, value)) - return true; + if (TryEnumIsDefined(type, value)) + return true; + if (TryEnumIsDefined(type, value)) + return true; + if (TryEnumIsDefined(type, value)) + return true; + if (TryEnumIsDefined(type, value)) + return true; + if (TryEnumIsDefined(type, value)) + return true; + if (TryEnumIsDefined(type, value)) + return true; + if (TryEnumIsDefined(type, value)) + return true; + if (TryEnumIsDefined(type, value)) + return true; + if (TryEnumIsDefined(type, value)) + return true; - return false; - } + return false; + } - public static bool TryEnumIsDefined(Type type, object value) + public static bool TryEnumIsDefined(Type type, object value) + { + // Catch any casting errors that can occur or if 0 is not defined as a default value. + try { - // Catch any casting errors that can occur or if 0 is not defined as a default value. - try - { - if (value is T && Enum.IsDefined(type, (T)value)) - return true; - } - catch (Exception) { } - - return false; + if (value is T && Enum.IsDefined(type, (T)value)) + return true; } + catch (Exception) { } + + return false; } } diff --git a/src/Foundatio/Extensions/EnumerableExtensions.cs b/src/Foundatio/Extensions/EnumerableExtensions.cs index 652b68b4e..aea7eded2 100644 --- a/src/Foundatio/Extensions/EnumerableExtensions.cs +++ b/src/Foundatio/Extensions/EnumerableExtensions.cs @@ -1,26 +1,25 @@ using System; using System.Collections.Generic; -namespace Foundatio.Utility +namespace Foundatio.Utility; + +internal static class EnumerableExtensions { - internal static class EnumerableExtensions + public static void ForEach(this IEnumerable collection, Action action) { - public static void ForEach(this IEnumerable collection, Action action) - { - if (collection == null || action == null) - return; + if (collection == null || action == null) + return; - foreach (var item in collection) - action(item); - } + foreach (var item in collection) + action(item); + } - public static void AddRange(this ICollection list, IEnumerable range) - { - if (list == null || range == null) - return; + public static void AddRange(this ICollection list, IEnumerable range) + { + if (list == null || range == null) + return; - foreach (var r in range) - list.Add(r); - } + foreach (var r in range) + list.Add(r); } } diff --git a/src/Foundatio/Extensions/ExceptionExtensions.cs b/src/Foundatio/Extensions/ExceptionExtensions.cs index ba0ac48b6..a438b934f 100644 --- a/src/Foundatio/Extensions/ExceptionExtensions.cs +++ b/src/Foundatio/Extensions/ExceptionExtensions.cs @@ -1,31 +1,30 @@ using System; using System.Linq; -namespace Foundatio.Utility +namespace Foundatio.Utility; + +internal static class ExceptionExtensions { - internal static class ExceptionExtensions + public static Exception GetInnermostException(this Exception exception) { - public static Exception GetInnermostException(this Exception exception) - { - if (exception == null) - return null; + if (exception == null) + return null; - Exception current = exception; - while (current.InnerException != null) - current = current.InnerException; + Exception current = exception; + while (current.InnerException != null) + current = current.InnerException; - return current; - } + return current; + } - public static string GetMessage(this Exception exception) - { - if (exception == null) - return String.Empty; + public static string GetMessage(this Exception exception) + { + if (exception == null) + return String.Empty; - if (exception is AggregateException aggregateException) - return String.Join(Environment.NewLine, aggregateException.Flatten().InnerExceptions.Where(ex => !String.IsNullOrEmpty(ex.GetInnermostException().Message)).Select(ex => ex.GetInnermostException().Message)); + if (exception is AggregateException aggregateException) + return String.Join(Environment.NewLine, aggregateException.Flatten().InnerExceptions.Where(ex => !String.IsNullOrEmpty(ex.GetInnermostException().Message)).Select(ex => ex.GetInnermostException().Message)); - return exception.GetInnermostException().Message; - } + return exception.GetInnermostException().Message; } } diff --git a/src/Foundatio/Extensions/LoggerExtensions.cs b/src/Foundatio/Extensions/LoggerExtensions.cs index 0b2b80a72..3e88f1686 100644 --- a/src/Foundatio/Extensions/LoggerExtensions.cs +++ b/src/Foundatio/Extensions/LoggerExtensions.cs @@ -3,140 +3,139 @@ using System.Collections.Generic; using System.Linq; -namespace Microsoft.Extensions.Logging +namespace Microsoft.Extensions.Logging; + +public class LogState : IEnumerable> { - public class LogState : IEnumerable> - { - private readonly Dictionary _state = new(); + private readonly Dictionary _state = new(); - public int Count => _state.Count; + public int Count => _state.Count; - public object this[string property] - { - get { return _state[property]; } - set { _state[property] = value; } - } + public object this[string property] + { + get { return _state[property]; } + set { _state[property] = value; } + } - public LogState Property(string property, object value) - { + public LogState Property(string property, object value) + { + _state.Add(property, value); + return this; + } + + public LogState PropertyIf(string property, object value, bool condition) + { + if (condition) _state.Add(property, value); - return this; - } + return this; + } - public LogState PropertyIf(string property, object value, bool condition) - { - if (condition) - _state.Add(property, value); - return this; - } + public bool ContainsProperty(string property) + { + return _state.ContainsKey(property); + } - public bool ContainsProperty(string property) - { - return _state.ContainsKey(property); - } + public IEnumerator> GetEnumerator() + { + return _state.GetEnumerator(); + } - public IEnumerator> GetEnumerator() - { - return _state.GetEnumerator(); - } + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } +} - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } +public static class LoggerExtensions +{ + public static IDisposable BeginScope(this ILogger logger, Func stateBuilder) + { + var logState = new LogState(); + logState = stateBuilder(logState); + return logger.BeginScope(logState); } - public static class LoggerExtensions + public static IDisposable BeginScope(this ILogger logger, string property, object value) { - public static IDisposable BeginScope(this ILogger logger, Func stateBuilder) - { - var logState = new LogState(); - logState = stateBuilder(logState); - return logger.BeginScope(logState); - } + return logger.BeginScope(b => b.Property(property, value)); + } - public static IDisposable BeginScope(this ILogger logger, string property, object value) - { - return logger.BeginScope(b => b.Property(property, value)); - } + public static void LogDebug(this ILogger logger, Func stateBuilder, string message, params object[] args) + { + using (BeginScope(logger, stateBuilder)) + logger.LogDebug(message, args); + } - public static void LogDebug(this ILogger logger, Func stateBuilder, string message, params object[] args) - { - using (BeginScope(logger, stateBuilder)) - logger.LogDebug(message, args); - } + public static void LogTrace(this ILogger logger, Func stateBuilder, string message, params object[] args) + { + using (BeginScope(logger, stateBuilder)) + logger.LogTrace(message, args); + } - public static void LogTrace(this ILogger logger, Func stateBuilder, string message, params object[] args) - { - using (BeginScope(logger, stateBuilder)) - logger.LogTrace(message, args); - } + public static void LogInformation(this ILogger logger, Func stateBuilder, string message, params object[] args) + { + using (BeginScope(logger, stateBuilder)) + logger.LogInformation(message, args); + } - public static void LogInformation(this ILogger logger, Func stateBuilder, string message, params object[] args) - { - using (BeginScope(logger, stateBuilder)) - logger.LogInformation(message, args); - } + public static void LogWarning(this ILogger logger, Func stateBuilder, string message, params object[] args) + { + using (BeginScope(logger, stateBuilder)) + logger.LogWarning(message, args); + } - public static void LogWarning(this ILogger logger, Func stateBuilder, string message, params object[] args) - { - using (BeginScope(logger, stateBuilder)) - logger.LogWarning(message, args); - } + public static void LogError(this ILogger logger, Func stateBuilder, string message, params object[] args) + { + using (BeginScope(logger, stateBuilder)) + logger.LogError(message, args); + } - public static void LogError(this ILogger logger, Func stateBuilder, string message, params object[] args) - { - using (BeginScope(logger, stateBuilder)) - logger.LogError(message, args); - } + public static void LogError(this ILogger logger, Func stateBuilder, Exception exception, string message, params object[] args) + { + using (BeginScope(logger, stateBuilder)) + logger.LogError(exception, message, args); + } - public static void LogError(this ILogger logger, Func stateBuilder, Exception exception, string message, params object[] args) - { - using (BeginScope(logger, stateBuilder)) - logger.LogError(exception, message, args); - } + public static void LogCritical(this ILogger logger, Func stateBuilder, string message, params object[] args) + { + using (BeginScope(logger, stateBuilder)) + logger.LogCritical(message, args); + } - public static void LogCritical(this ILogger logger, Func stateBuilder, string message, params object[] args) - { - using (BeginScope(logger, stateBuilder)) - logger.LogCritical(message, args); - } + public static LogState Critical(this LogState builder, bool isCritical = true) + { + return isCritical ? builder.Tag("Critical") : builder; + } - public static LogState Critical(this LogState builder, bool isCritical = true) - { - return isCritical ? builder.Tag("Critical") : builder; - } + public static LogState Tag(this LogState builder, string tag) + { + return builder.Tag(new[] { tag }); + } - public static LogState Tag(this LogState builder, string tag) - { - return builder.Tag(new[] { tag }); - } + public static LogState Tag(this LogState builder, IEnumerable tags) + { + var tagList = new List(); + if (builder.ContainsProperty("Tags") && builder["Tags"] is List) + tagList = builder["Tags"] as List; - public static LogState Tag(this LogState builder, IEnumerable tags) + foreach (string tag in tags) { - var tagList = new List(); - if (builder.ContainsProperty("Tags") && builder["Tags"] is List) - tagList = builder["Tags"] as List; - - foreach (string tag in tags) - { - if (!tagList.Any(s => s.Equals(tag, StringComparison.OrdinalIgnoreCase))) - tagList.Add(tag); - } - - return builder.Property("Tags", tagList); + if (!tagList.Any(s => s.Equals(tag, StringComparison.OrdinalIgnoreCase))) + tagList.Add(tag); } - public static LogState Properties(this LogState builder, ICollection> collection) - { - if (collection == null) - return builder; - - foreach (var pair in collection) - if (pair.Key != null) - builder.Property(pair.Key, pair.Value); + return builder.Property("Tags", tagList); + } + public static LogState Properties(this LogState builder, ICollection> collection) + { + if (collection == null) return builder; - } + + foreach (var pair in collection) + if (pair.Key != null) + builder.Property(pair.Key, pair.Value); + + return builder; } } diff --git a/src/Foundatio/Extensions/NumberExtensions.cs b/src/Foundatio/Extensions/NumberExtensions.cs index 12b716480..5f57ee293 100644 --- a/src/Foundatio/Extensions/NumberExtensions.cs +++ b/src/Foundatio/Extensions/NumberExtensions.cs @@ -1,65 +1,64 @@ using System; -namespace Foundatio.Utility +namespace Foundatio.Utility; + +internal static class NumericExtensions { - internal static class NumericExtensions + public static string ToFileSizeDisplay(this int i) { - public static string ToFileSizeDisplay(this int i) - { - return ToFileSizeDisplay((long)i, 2); - } + return ToFileSizeDisplay((long)i, 2); + } - public static string ToFileSizeDisplay(this int i, int decimals) - { - return ToFileSizeDisplay((long)i, decimals); - } + public static string ToFileSizeDisplay(this int i, int decimals) + { + return ToFileSizeDisplay((long)i, decimals); + } - public static string ToFileSizeDisplay(this long i) - { - return ToFileSizeDisplay(i, 2); - } + public static string ToFileSizeDisplay(this long i) + { + return ToFileSizeDisplay(i, 2); + } - public static string ToFileSizeDisplay(this long i, int decimals) + public static string ToFileSizeDisplay(this long i, int decimals) + { + if (i < 1024 * 1024 * 1024) // 1 GB { - if (i < 1024 * 1024 * 1024) // 1 GB - { - string value = Math.Round((decimal)i / 1024m / 1024m, decimals).ToString("N" + decimals); - if (decimals > 0 && value.EndsWith(new string('0', decimals))) - value = value.Substring(0, value.Length - decimals - 1); + string value = Math.Round((decimal)i / 1024m / 1024m, decimals).ToString("N" + decimals); + if (decimals > 0 && value.EndsWith(new string('0', decimals))) + value = value.Substring(0, value.Length - decimals - 1); - return String.Concat(value, " MB"); - } - else - { - string value = Math.Round((decimal)i / 1024m / 1024m / 1024m, decimals).ToString("N" + decimals); - if (decimals > 0 && value.EndsWith(new string('0', decimals))) - value = value.Substring(0, value.Length - decimals - 1); + return String.Concat(value, " MB"); + } + else + { + string value = Math.Round((decimal)i / 1024m / 1024m / 1024m, decimals).ToString("N" + decimals); + if (decimals > 0 && value.EndsWith(new string('0', decimals))) + value = value.Substring(0, value.Length - decimals - 1); - return String.Concat(value, " GB"); - } + return String.Concat(value, " GB"); } + } - public static string ToOrdinal(this int num) + public static string ToOrdinal(this int num) + { + switch (num % 100) { - switch (num % 100) - { - case 11: - case 12: - case 13: - return num.ToString("#,###0") + "th"; - } + case 11: + case 12: + case 13: + return num.ToString("#,###0") + "th"; + } - switch (num % 10) - { - case 1: - return num.ToString("#,###0") + "st"; - case 2: - return num.ToString("#,###0") + "nd"; - case 3: - return num.ToString("#,###0") + "rd"; - default: - return num.ToString("#,###0") + "th"; - } + switch (num % 10) + { + case 1: + return num.ToString("#,###0") + "st"; + case 2: + return num.ToString("#,###0") + "nd"; + case 3: + return num.ToString("#,###0") + "rd"; + default: + return num.ToString("#,###0") + "th"; } } } diff --git a/src/Foundatio/Extensions/ObjectExtensions.cs b/src/Foundatio/Extensions/ObjectExtensions.cs index f39fd1aa4..96f3af885 100644 --- a/src/Foundatio/Extensions/ObjectExtensions.cs +++ b/src/Foundatio/Extensions/ObjectExtensions.cs @@ -1,13 +1,12 @@ using System; using Foundatio.Force.DeepCloner.Helpers; -namespace Foundatio.Utility +namespace Foundatio.Utility; + +public static class ObjectExtensions { - public static class ObjectExtensions + public static T DeepClone(this T original) { - public static T DeepClone(this T original) - { - return DeepClonerGenerator.CloneObject(original); - } + return DeepClonerGenerator.CloneObject(original); } } diff --git a/src/Foundatio/Extensions/StringExtensions.cs b/src/Foundatio/Extensions/StringExtensions.cs index 2fa849393..97abdd443 100644 --- a/src/Foundatio/Extensions/StringExtensions.cs +++ b/src/Foundatio/Extensions/StringExtensions.cs @@ -1,21 +1,20 @@ using System; using System.IO; -namespace Foundatio.Extensions +namespace Foundatio.Extensions; + +internal static class StringExtensions { - internal static class StringExtensions + public static string NormalizePath(this string path) { - public static string NormalizePath(this string path) - { - if (String.IsNullOrEmpty(path)) - return path; + if (String.IsNullOrEmpty(path)) + return path; - if (Path.DirectorySeparatorChar == '\\') - path = path.Replace('/', Path.DirectorySeparatorChar); - else if (Path.DirectorySeparatorChar == '/') - path = path.Replace('\\', Path.DirectorySeparatorChar); + if (Path.DirectorySeparatorChar == '\\') + path = path.Replace('/', Path.DirectorySeparatorChar); + else if (Path.DirectorySeparatorChar == '/') + path = path.Replace('\\', Path.DirectorySeparatorChar); - return path; - } + return path; } } diff --git a/src/Foundatio/Extensions/TaskExtensions.cs b/src/Foundatio/Extensions/TaskExtensions.cs index c5e2f03e5..8178c0c8e 100644 --- a/src/Foundatio/Extensions/TaskExtensions.cs +++ b/src/Foundatio/Extensions/TaskExtensions.cs @@ -5,44 +5,43 @@ using System.Threading.Tasks; using Foundatio.AsyncEx; -namespace Foundatio.Utility +namespace Foundatio.Utility; + +internal static class TaskExtensions { - internal static class TaskExtensions + [DebuggerStepThrough] + public static ConfiguredTaskAwaitable AnyContext(this Task task) { - [DebuggerStepThrough] - public static ConfiguredTaskAwaitable AnyContext(this Task task) - { - return task.ConfigureAwait(continueOnCapturedContext: false); - } + return task.ConfigureAwait(continueOnCapturedContext: false); + } - [DebuggerStepThrough] - public static ConfiguredCancelableAsyncEnumerable AnyContext(this IAsyncEnumerable source) - { - return source.ConfigureAwait(continueOnCapturedContext: false); - } + [DebuggerStepThrough] + public static ConfiguredCancelableAsyncEnumerable AnyContext(this IAsyncEnumerable source) + { + return source.ConfigureAwait(continueOnCapturedContext: false); + } - [DebuggerStepThrough] - public static ConfiguredAsyncDisposable AnyContext(this IAsyncDisposable source) - { - return source.ConfigureAwait(continueOnCapturedContext: false); - } + [DebuggerStepThrough] + public static ConfiguredAsyncDisposable AnyContext(this IAsyncDisposable source) + { + return source.ConfigureAwait(continueOnCapturedContext: false); + } - [DebuggerStepThrough] - public static ConfiguredTaskAwaitable AnyContext(this Task task) - { - return task.ConfigureAwait(continueOnCapturedContext: false); - } + [DebuggerStepThrough] + public static ConfiguredTaskAwaitable AnyContext(this Task task) + { + return task.ConfigureAwait(continueOnCapturedContext: false); + } - [DebuggerStepThrough] - public static ConfiguredValueTaskAwaitable AnyContext(this ValueTask task) - { - return task.ConfigureAwait(continueOnCapturedContext: false); - } + [DebuggerStepThrough] + public static ConfiguredValueTaskAwaitable AnyContext(this ValueTask task) + { + return task.ConfigureAwait(continueOnCapturedContext: false); + } - [DebuggerStepThrough] - public static ConfiguredTaskAwaitable AnyContext(this AwaitableDisposable task) where TResult : IDisposable - { - return task.ConfigureAwait(continueOnCapturedContext: false); - } + [DebuggerStepThrough] + public static ConfiguredTaskAwaitable AnyContext(this AwaitableDisposable task) where TResult : IDisposable + { + return task.ConfigureAwait(continueOnCapturedContext: false); } } diff --git a/src/Foundatio/Extensions/TimespanExtensions.cs b/src/Foundatio/Extensions/TimespanExtensions.cs index c3af315dc..62d4c8d26 100644 --- a/src/Foundatio/Extensions/TimespanExtensions.cs +++ b/src/Foundatio/Extensions/TimespanExtensions.cs @@ -1,46 +1,45 @@ using System; using System.Threading; -namespace Foundatio.Utility +namespace Foundatio.Utility; + +internal static class TimeSpanExtensions { - internal static class TimeSpanExtensions + public static CancellationTokenSource ToCancellationTokenSource(this TimeSpan timeout) { - public static CancellationTokenSource ToCancellationTokenSource(this TimeSpan timeout) + if (timeout == TimeSpan.Zero) { - if (timeout == TimeSpan.Zero) - { - var source = new CancellationTokenSource(); - source.Cancel(); - return source; - } + var source = new CancellationTokenSource(); + source.Cancel(); + return source; + } - if (timeout.Ticks > 0) - return new CancellationTokenSource(timeout); + if (timeout.Ticks > 0) + return new CancellationTokenSource(timeout); - return new CancellationTokenSource(); - } + return new CancellationTokenSource(); + } - public static CancellationTokenSource ToCancellationTokenSource(this TimeSpan? timeout) - { - if (timeout.HasValue) - return timeout.Value.ToCancellationTokenSource(); + public static CancellationTokenSource ToCancellationTokenSource(this TimeSpan? timeout) + { + if (timeout.HasValue) + return timeout.Value.ToCancellationTokenSource(); - return new CancellationTokenSource(); - } + return new CancellationTokenSource(); + } - public static CancellationTokenSource ToCancellationTokenSource(this TimeSpan? timeout, TimeSpan defaultTimeout) - { - return (timeout ?? defaultTimeout).ToCancellationTokenSource(); - } + public static CancellationTokenSource ToCancellationTokenSource(this TimeSpan? timeout, TimeSpan defaultTimeout) + { + return (timeout ?? defaultTimeout).ToCancellationTokenSource(); + } - public static TimeSpan Min(this TimeSpan source, TimeSpan other) - { - return source.Ticks > other.Ticks ? other : source; - } + public static TimeSpan Min(this TimeSpan source, TimeSpan other) + { + return source.Ticks > other.Ticks ? other : source; + } - public static TimeSpan Max(this TimeSpan source, TimeSpan other) - { - return source.Ticks < other.Ticks ? other : source; - } + public static TimeSpan Max(this TimeSpan source, TimeSpan other) + { + return source.Ticks < other.Ticks ? other : source; } } diff --git a/src/Foundatio/Extensions/TypeExtensions.cs b/src/Foundatio/Extensions/TypeExtensions.cs index 092f25dec..1cb328881 100644 --- a/src/Foundatio/Extensions/TypeExtensions.cs +++ b/src/Foundatio/Extensions/TypeExtensions.cs @@ -3,129 +3,128 @@ using System.Reflection; using Foundatio.Serializer; -namespace Foundatio.Utility +namespace Foundatio.Utility; + +internal static class TypeExtensions { - internal static class TypeExtensions + public static bool IsNumeric(this Type type) { - public static bool IsNumeric(this Type type) + if (type.IsArray) + return false; + + if (type == TypeHelper.ByteType || + type == TypeHelper.DecimalType || + type == TypeHelper.DoubleType || + type == TypeHelper.Int16Type || + type == TypeHelper.Int32Type || + type == TypeHelper.Int64Type || + type == TypeHelper.SByteType || + type == TypeHelper.SingleType || + type == TypeHelper.UInt16Type || + type == TypeHelper.UInt32Type || + type == TypeHelper.UInt64Type) + return true; + + switch (Type.GetTypeCode(type)) { - if (type.IsArray) - return false; - - if (type == TypeHelper.ByteType || - type == TypeHelper.DecimalType || - type == TypeHelper.DoubleType || - type == TypeHelper.Int16Type || - type == TypeHelper.Int32Type || - type == TypeHelper.Int64Type || - type == TypeHelper.SByteType || - type == TypeHelper.SingleType || - type == TypeHelper.UInt16Type || - type == TypeHelper.UInt32Type || - type == TypeHelper.UInt64Type) + case TypeCode.Byte: + case TypeCode.Decimal: + case TypeCode.Double: + case TypeCode.Int16: + case TypeCode.Int32: + case TypeCode.Int64: + case TypeCode.SByte: + case TypeCode.Single: + case TypeCode.UInt16: + case TypeCode.UInt32: + case TypeCode.UInt64: return true; + } - switch (Type.GetTypeCode(type)) - { - case TypeCode.Byte: - case TypeCode.Decimal: - case TypeCode.Double: - case TypeCode.Int16: - case TypeCode.Int32: - case TypeCode.Int64: - case TypeCode.SByte: - case TypeCode.Single: - case TypeCode.UInt16: - case TypeCode.UInt32: - case TypeCode.UInt64: - return true; - } + return false; + } + public static bool IsNullableNumeric(this Type type) + { + if (type.IsArray) return false; - } - public static bool IsNullableNumeric(this Type type) - { - if (type.IsArray) - return false; - - var t = Nullable.GetUnderlyingType(type); - return t != null && t.IsNumeric(); - } + var t = Nullable.GetUnderlyingType(type); + return t != null && t.IsNumeric(); + } - public static T ToType(this object value, ISerializer serializer = null) + public static T ToType(this object value, ISerializer serializer = null) + { + var targetType = typeof(T); + if (value == null) { - var targetType = typeof(T); - if (value == null) + try + { + return (T)Convert.ChangeType(value, targetType); + } + catch { - try - { - return (T)Convert.ChangeType(value, targetType); - } - catch - { - throw new ArgumentNullException(nameof(value)); - } + throw new ArgumentNullException(nameof(value)); } + } - var converter = TypeDescriptor.GetConverter(targetType); - var valueType = value.GetType(); + var converter = TypeDescriptor.GetConverter(targetType); + var valueType = value.GetType(); - if (targetType.IsAssignableFrom(valueType)) - return (T)value; + if (targetType.IsAssignableFrom(valueType)) + return (T)value; - var targetTypeInfo = targetType.GetTypeInfo(); - if (targetTypeInfo.IsEnum && (value is string || valueType.GetTypeInfo().IsEnum)) + var targetTypeInfo = targetType.GetTypeInfo(); + if (targetTypeInfo.IsEnum && (value is string || valueType.GetTypeInfo().IsEnum)) + { + // attempt to match enum by name. + if (EnumExtensions.TryEnumIsDefined(targetType, value.ToString())) { - // attempt to match enum by name. - if (EnumExtensions.TryEnumIsDefined(targetType, value.ToString())) - { - object parsedValue = Enum.Parse(targetType, value.ToString(), false); - return (T)parsedValue; - } - - string message = $"The Enum value of '{value}' is not defined as a valid value for '{targetType.FullName}'."; - throw new ArgumentException(message); + object parsedValue = Enum.Parse(targetType, value.ToString(), false); + return (T)parsedValue; } - if (targetTypeInfo.IsEnum && valueType.IsNumeric()) - return (T)Enum.ToObject(targetType, value); + string message = $"The Enum value of '{value}' is not defined as a valid value for '{targetType.FullName}'."; + throw new ArgumentException(message); + } - if (converter.CanConvertFrom(valueType)) - { - object convertedValue = converter.ConvertFrom(value); - return (T)convertedValue; - } + if (targetTypeInfo.IsEnum && valueType.IsNumeric()) + return (T)Enum.ToObject(targetType, value); + + if (converter.CanConvertFrom(valueType)) + { + object convertedValue = converter.ConvertFrom(value); + return (T)convertedValue; + } - if (serializer != null && value is byte[] data) + if (serializer != null && value is byte[] data) + { + try { - try - { - return serializer.Deserialize(data); - } - catch { } + return serializer.Deserialize(data); } + catch { } + } - if (serializer != null && value is string stringValue) + if (serializer != null && value is string stringValue) + { + try { - try - { - return serializer.Deserialize(stringValue); - } - catch { } + return serializer.Deserialize(stringValue); } + catch { } + } - if (value is IConvertible) + if (value is IConvertible) + { + try { - try - { - object convertedValue = Convert.ChangeType(value, targetType); - return (T)convertedValue; - } - catch { } + object convertedValue = Convert.ChangeType(value, targetType); + return (T)convertedValue; } - - throw new ArgumentException($"An incompatible value specified. Target Type: {targetType.FullName} Value Type: {value.GetType().FullName}", nameof(value)); + catch { } } + + throw new ArgumentException($"An incompatible value specified. Target Type: {targetType.FullName} Value Type: {value.GetType().FullName}", nameof(value)); } } diff --git a/src/Foundatio/Jobs/IJob.cs b/src/Foundatio/Jobs/IJob.cs index 039d5d9a5..fec87b32e 100644 --- a/src/Foundatio/Jobs/IJob.cs +++ b/src/Foundatio/Jobs/IJob.cs @@ -6,88 +6,87 @@ using Foundatio.Utility; using Microsoft.Extensions.Logging; -namespace Foundatio.Jobs +namespace Foundatio.Jobs; + +public interface IJob { - public interface IJob - { - Task RunAsync(CancellationToken cancellationToken = default); - } + Task RunAsync(CancellationToken cancellationToken = default); +} - public static class JobExtensions +public static class JobExtensions +{ + public static async Task TryRunAsync(this IJob job, CancellationToken cancellationToken = default) { - public static async Task TryRunAsync(this IJob job, CancellationToken cancellationToken = default) + try { - try - { - return await job.RunAsync(cancellationToken).AnyContext(); - } - catch (OperationCanceledException) - { - return JobResult.Cancelled; - } - catch (Exception ex) - { - return JobResult.FromException(ex); - } + return await job.RunAsync(cancellationToken).AnyContext(); + } + catch (OperationCanceledException) + { + return JobResult.Cancelled; + } + catch (Exception ex) + { + return JobResult.FromException(ex); } + } + + public static async Task RunContinuousAsync(this IJob job, TimeSpan? interval = null, int iterationLimit = -1, CancellationToken cancellationToken = default, Func> continuationCallback = null) + { + int iterations = 0; + string jobName = job.GetType().Name; + var logger = job.GetLogger(); - public static async Task RunContinuousAsync(this IJob job, TimeSpan? interval = null, int iterationLimit = -1, CancellationToken cancellationToken = default, Func> continuationCallback = null) + using (logger.BeginScope(new Dictionary { { "job", jobName } })) { - int iterations = 0; - string jobName = job.GetType().Name; - var logger = job.GetLogger(); + if (logger.IsEnabled(LogLevel.Information)) + logger.LogInformation("Starting continuous job type {JobName} on machine {MachineName}...", jobName, Environment.MachineName); - using (logger.BeginScope(new Dictionary { { "job", jobName } })) + while (!cancellationToken.IsCancellationRequested) { - if (logger.IsEnabled(LogLevel.Information)) - logger.LogInformation("Starting continuous job type {JobName} on machine {MachineName}...", jobName, Environment.MachineName); + var result = await job.TryRunAsync(cancellationToken).AnyContext(); + logger.LogJobResult(result, jobName); + iterations++; + + if (cancellationToken.IsCancellationRequested || (iterationLimit > -1 && iterationLimit <= iterations)) + break; - while (!cancellationToken.IsCancellationRequested) + if (result.Error != null) { - var result = await job.TryRunAsync(cancellationToken).AnyContext(); - logger.LogJobResult(result, jobName); - iterations++; + await SystemClock.SleepSafeAsync(Math.Max((int)(interval?.TotalMilliseconds ?? 0), 100), cancellationToken).AnyContext(); + } + else if (interval.HasValue && interval.Value > TimeSpan.Zero) + { + await SystemClock.SleepSafeAsync(interval.Value, cancellationToken).AnyContext(); + } - if (cancellationToken.IsCancellationRequested || (iterationLimit > -1 && iterationLimit <= iterations)) - break; + // needed to yield back a task for jobs that aren't async + await Task.Yield(); - if (result.Error != null) - { - await SystemClock.SleepSafeAsync(Math.Max((int)(interval?.TotalMilliseconds ?? 0), 100), cancellationToken).AnyContext(); - } - else if (interval.HasValue && interval.Value > TimeSpan.Zero) - { - await SystemClock.SleepSafeAsync(interval.Value, cancellationToken).AnyContext(); - } + if (cancellationToken.IsCancellationRequested) + break; - // needed to yield back a task for jobs that aren't async - await Task.Yield(); + if (continuationCallback == null) + continue; - if (cancellationToken.IsCancellationRequested) + try + { + if (!await continuationCallback().AnyContext()) break; - - if (continuationCallback == null) - continue; - - try - { - if (!await continuationCallback().AnyContext()) - break; - } - catch (Exception ex) - { - if (logger.IsEnabled(LogLevel.Error)) - logger.LogError(ex, "Error in continuation callback: {Message}", ex.Message); - } } + catch (Exception ex) + { + if (logger.IsEnabled(LogLevel.Error)) + logger.LogError(ex, "Error in continuation callback: {Message}", ex.Message); + } + } - logger.LogInformation("Finished continuous job type {JobName}: {IterationLimit} {Iterations}", jobName, Environment.MachineName, iterationLimit, iterations); - if (cancellationToken.IsCancellationRequested && logger.IsEnabled(LogLevel.Trace)) - logger.LogTrace("Job cancellation requested"); + logger.LogInformation("Finished continuous job type {JobName}: {IterationLimit} {Iterations}", jobName, Environment.MachineName, iterationLimit, iterations); + if (cancellationToken.IsCancellationRequested && logger.IsEnabled(LogLevel.Trace)) + logger.LogTrace("Job cancellation requested"); - if (logger.IsEnabled(LogLevel.Information)) - logger.LogInformation("Stopping continuous job type {JobName} on machine {MachineName}...", jobName, Environment.MachineName); - } + if (logger.IsEnabled(LogLevel.Information)) + logger.LogInformation("Stopping continuous job type {JobName} on machine {MachineName}...", jobName, Environment.MachineName); } } } diff --git a/src/Foundatio/Jobs/IQueueJob.cs b/src/Foundatio/Jobs/IQueueJob.cs index 19eff3eda..cf8819653 100644 --- a/src/Foundatio/Jobs/IQueueJob.cs +++ b/src/Foundatio/Jobs/IQueueJob.cs @@ -6,34 +6,33 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -namespace Foundatio.Jobs +namespace Foundatio.Jobs; + +public interface IQueueJob : IJob where T : class { - public interface IQueueJob : IJob where T : class - { - /// - /// Processes a queue entry and returns the result. This method is typically called from RunAsync() - /// but can also be called from a function passing in the queue entry. - /// - Task ProcessAsync(IQueueEntry queueEntry, CancellationToken cancellationToken); - IQueue Queue { get; } - } + /// + /// Processes a queue entry and returns the result. This method is typically called from RunAsync() + /// but can also be called from a function passing in the queue entry. + /// + Task ProcessAsync(IQueueEntry queueEntry, CancellationToken cancellationToken); + IQueue Queue { get; } +} - public static class QueueJobExtensions +public static class QueueJobExtensions +{ + public static Task RunUntilEmptyAsync(this IQueueJob job, CancellationToken cancellationToken = default) where T : class { - public static Task RunUntilEmptyAsync(this IQueueJob job, CancellationToken cancellationToken = default) where T : class + var logger = job.GetLogger(); + return job.RunContinuousAsync(cancellationToken: cancellationToken, continuationCallback: async () => { - var logger = job.GetLogger(); - return job.RunContinuousAsync(cancellationToken: cancellationToken, continuationCallback: async () => - { - // Allow abandoned items to be added in a background task. - Thread.Yield(); + // Allow abandoned items to be added in a background task. + Thread.Yield(); - var stats = await job.Queue.GetQueueStatsAsync().AnyContext(); - if (logger.IsEnabled(LogLevel.Trace)) - logger.LogTrace("RunUntilEmpty continuation: Queued={Queued}, Working={Working}, Abandoned={Abandoned}", stats.Queued, stats.Working, stats.Abandoned); + var stats = await job.Queue.GetQueueStatsAsync().AnyContext(); + if (logger.IsEnabled(LogLevel.Trace)) + logger.LogTrace("RunUntilEmpty continuation: Queued={Queued}, Working={Working}, Abandoned={Abandoned}", stats.Queued, stats.Working, stats.Abandoned); - return stats.Queued + stats.Working > 0; - }); - } + return stats.Queued + stats.Working > 0; + }); } } diff --git a/src/Foundatio/Jobs/JobAttribute.cs b/src/Foundatio/Jobs/JobAttribute.cs index 65a26889d..d592b210a 100644 --- a/src/Foundatio/Jobs/JobAttribute.cs +++ b/src/Foundatio/Jobs/JobAttribute.cs @@ -1,16 +1,15 @@ using System; -namespace Foundatio.Jobs +namespace Foundatio.Jobs; + +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] +public class JobAttribute : Attribute { - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] - public class JobAttribute : Attribute - { - public string Name { get; set; } - public string Description { get; set; } - public bool IsContinuous { get; set; } = true; - public string Interval { get; set; } - public string InitialDelay { get; set; } - public int IterationLimit { get; set; } = -1; - public int InstanceCount { get; set; } = 1; - } + public string Name { get; set; } + public string Description { get; set; } + public bool IsContinuous { get; set; } = true; + public string Interval { get; set; } + public string InitialDelay { get; set; } + public int IterationLimit { get; set; } = -1; + public int InstanceCount { get; set; } = 1; } diff --git a/src/Foundatio/Jobs/JobBase.cs b/src/Foundatio/Jobs/JobBase.cs index 599ce99fc..c26f86313 100644 --- a/src/Foundatio/Jobs/JobBase.cs +++ b/src/Foundatio/Jobs/JobBase.cs @@ -5,25 +5,24 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -namespace Foundatio.Jobs -{ - public abstract class JobBase : IJob, IHaveLogger - { - protected readonly ILogger _logger; +namespace Foundatio.Jobs; - public JobBase(ILoggerFactory loggerFactory = null) - { - _logger = loggerFactory?.CreateLogger(GetType()) ?? NullLogger.Instance; - } +public abstract class JobBase : IJob, IHaveLogger +{ + protected readonly ILogger _logger; - public string JobId { get; } = Guid.NewGuid().ToString("N").Substring(0, 10); - ILogger IHaveLogger.Logger => _logger; + public JobBase(ILoggerFactory loggerFactory = null) + { + _logger = loggerFactory?.CreateLogger(GetType()) ?? NullLogger.Instance; + } - public virtual Task RunAsync(CancellationToken cancellationToken = default) - { - return RunInternalAsync(new JobContext(cancellationToken)); - } + public string JobId { get; } = Guid.NewGuid().ToString("N").Substring(0, 10); + ILogger IHaveLogger.Logger => _logger; - protected abstract Task RunInternalAsync(JobContext context); + public virtual Task RunAsync(CancellationToken cancellationToken = default) + { + return RunInternalAsync(new JobContext(cancellationToken)); } + + protected abstract Task RunInternalAsync(JobContext context); } diff --git a/src/Foundatio/Jobs/JobContext.cs b/src/Foundatio/Jobs/JobContext.cs index 8755d7491..85db46d7b 100644 --- a/src/Foundatio/Jobs/JobContext.cs +++ b/src/Foundatio/Jobs/JobContext.cs @@ -2,25 +2,24 @@ using System.Threading.Tasks; using Foundatio.Lock; -namespace Foundatio.Jobs +namespace Foundatio.Jobs; + +public class JobContext { - public class JobContext + public JobContext(CancellationToken cancellationToken, ILock lck = null) { - public JobContext(CancellationToken cancellationToken, ILock lck = null) - { - Lock = lck; - CancellationToken = cancellationToken; - } + Lock = lck; + CancellationToken = cancellationToken; + } - public ILock Lock { get; } - public CancellationToken CancellationToken { get; } + public ILock Lock { get; } + public CancellationToken CancellationToken { get; } - public virtual Task RenewLockAsync() - { - if (Lock != null) - return Lock.RenewAsync(); + public virtual Task RenewLockAsync() + { + if (Lock != null) + return Lock.RenewAsync(); - return Task.CompletedTask; - } + return Task.CompletedTask; } } diff --git a/src/Foundatio/Jobs/JobOptions.cs b/src/Foundatio/Jobs/JobOptions.cs index 338e0dc5a..1b69bbb15 100644 --- a/src/Foundatio/Jobs/JobOptions.cs +++ b/src/Foundatio/Jobs/JobOptions.cs @@ -2,100 +2,99 @@ using System.Reflection; using Foundatio.Utility; -namespace Foundatio.Jobs +namespace Foundatio.Jobs; + +public class JobOptions { - public class JobOptions + public string Name { get; set; } + public string Description { get; set; } + public Func JobFactory { get; set; } + public bool RunContinuous { get; set; } = true; + public TimeSpan? Interval { get; set; } + public TimeSpan? InitialDelay { get; set; } + public int IterationLimit { get; set; } = -1; + public int InstanceCount { get; set; } = 1; + + public static JobOptions GetDefaults(Type jobType) { - public string Name { get; set; } - public string Description { get; set; } - public Func JobFactory { get; set; } - public bool RunContinuous { get; set; } = true; - public TimeSpan? Interval { get; set; } - public TimeSpan? InitialDelay { get; set; } - public int IterationLimit { get; set; } = -1; - public int InstanceCount { get; set; } = 1; - - public static JobOptions GetDefaults(Type jobType) - { - var jobOptions = new JobOptions(); - ApplyDefaults(jobOptions, jobType); - return jobOptions; - } + var jobOptions = new JobOptions(); + ApplyDefaults(jobOptions, jobType); + return jobOptions; + } - public static void ApplyDefaults(JobOptions jobOptions, Type jobType) - { - var jobAttribute = jobType.GetCustomAttribute() ?? new JobAttribute(); - - jobOptions.Name = jobAttribute.Name; - if (String.IsNullOrEmpty(jobOptions.Name)) - { - string jobName = jobType.Name; - if (jobName.EndsWith("Job")) - jobName = jobName.Substring(0, jobName.Length - 3); - - jobOptions.Name = jobName.ToLower(); - } - - jobOptions.Description = jobAttribute.Description; - jobOptions.RunContinuous = jobAttribute.IsContinuous; - - if (!String.IsNullOrEmpty(jobAttribute.Interval)) - { - TimeSpan? interval; - if (TimeUnit.TryParse(jobAttribute.Interval, out interval)) - jobOptions.Interval = interval; - } - - if (!String.IsNullOrEmpty(jobAttribute.InitialDelay)) - { - TimeSpan? delay; - if (TimeUnit.TryParse(jobAttribute.InitialDelay, out delay)) - jobOptions.InitialDelay = delay; - } - - jobOptions.IterationLimit = jobAttribute.IterationLimit; - jobOptions.InstanceCount = jobAttribute.InstanceCount; - } + public static void ApplyDefaults(JobOptions jobOptions, Type jobType) + { + var jobAttribute = jobType.GetCustomAttribute() ?? new JobAttribute(); - public static JobOptions GetDefaults() where T : IJob + jobOptions.Name = jobAttribute.Name; + if (String.IsNullOrEmpty(jobOptions.Name)) { - return GetDefaults(typeof(T)); - } + string jobName = jobType.Name; + if (jobName.EndsWith("Job")) + jobName = jobName.Substring(0, jobName.Length - 3); - public static JobOptions GetDefaults(IJob instance) - { - var jobOptions = GetDefaults(instance.GetType()); - jobOptions.JobFactory = () => instance; - return jobOptions; + jobOptions.Name = jobName.ToLower(); } - public static JobOptions GetDefaults(IJob instance) where T : IJob - { - var jobOptions = GetDefaults(); - jobOptions.JobFactory = () => instance; - return jobOptions; - } + jobOptions.Description = jobAttribute.Description; + jobOptions.RunContinuous = jobAttribute.IsContinuous; - public static JobOptions GetDefaults(Type jobType, Func jobFactory) + if (!String.IsNullOrEmpty(jobAttribute.Interval)) { - var jobOptions = GetDefaults(jobType); - jobOptions.JobFactory = jobFactory; - return jobOptions; + TimeSpan? interval; + if (TimeUnit.TryParse(jobAttribute.Interval, out interval)) + jobOptions.Interval = interval; } - public static JobOptions GetDefaults(Func jobFactory) where T : IJob + if (!String.IsNullOrEmpty(jobAttribute.InitialDelay)) { - var jobOptions = GetDefaults(); - jobOptions.JobFactory = jobFactory; - return jobOptions; + TimeSpan? delay; + if (TimeUnit.TryParse(jobAttribute.InitialDelay, out delay)) + jobOptions.InitialDelay = delay; } + + jobOptions.IterationLimit = jobAttribute.IterationLimit; + jobOptions.InstanceCount = jobAttribute.InstanceCount; } - public static class JobOptionExtensions + public static JobOptions GetDefaults() where T : IJob { - public static void ApplyDefaults(this JobOptions jobOptions) - { - JobOptions.ApplyDefaults(jobOptions, typeof(T)); - } + return GetDefaults(typeof(T)); + } + + public static JobOptions GetDefaults(IJob instance) + { + var jobOptions = GetDefaults(instance.GetType()); + jobOptions.JobFactory = () => instance; + return jobOptions; + } + + public static JobOptions GetDefaults(IJob instance) where T : IJob + { + var jobOptions = GetDefaults(); + jobOptions.JobFactory = () => instance; + return jobOptions; + } + + public static JobOptions GetDefaults(Type jobType, Func jobFactory) + { + var jobOptions = GetDefaults(jobType); + jobOptions.JobFactory = jobFactory; + return jobOptions; + } + + public static JobOptions GetDefaults(Func jobFactory) where T : IJob + { + var jobOptions = GetDefaults(); + jobOptions.JobFactory = jobFactory; + return jobOptions; + } +} + +public static class JobOptionExtensions +{ + public static void ApplyDefaults(this JobOptions jobOptions) + { + JobOptions.ApplyDefaults(jobOptions, typeof(T)); } } diff --git a/src/Foundatio/Jobs/JobResult.cs b/src/Foundatio/Jobs/JobResult.cs index ae1cc63b5..1229482aa 100644 --- a/src/Foundatio/Jobs/JobResult.cs +++ b/src/Foundatio/Jobs/JobResult.cs @@ -1,89 +1,88 @@ using System; using Microsoft.Extensions.Logging; -namespace Foundatio.Jobs +namespace Foundatio.Jobs; + +public class JobResult { - public class JobResult + public bool IsCancelled { get; set; } + public Exception Error { get; set; } + public string Message { get; set; } + public bool IsSuccess { get; set; } + + public static readonly JobResult None = new() { - public bool IsCancelled { get; set; } - public Exception Error { get; set; } - public string Message { get; set; } - public bool IsSuccess { get; set; } + IsSuccess = true, + Message = String.Empty + }; - public static readonly JobResult None = new() - { - IsSuccess = true, - Message = String.Empty - }; + public static readonly JobResult Cancelled = new() + { + IsCancelled = true + }; - public static readonly JobResult Cancelled = new() - { - IsCancelled = true - }; + public static readonly JobResult Success = new() + { + IsSuccess = true + }; - public static readonly JobResult Success = new() + public static JobResult FromException(Exception exception, string message = null) + { + return new JobResult { - IsSuccess = true + Error = exception, + IsSuccess = false, + Message = message ?? exception.Message }; + } - public static JobResult FromException(Exception exception, string message = null) - { - return new JobResult - { - Error = exception, - IsSuccess = false, - Message = message ?? exception.Message - }; - } - - public static JobResult CancelledWithMessage(string message) + public static JobResult CancelledWithMessage(string message) + { + return new JobResult { - return new JobResult - { - IsCancelled = true, - Message = message - }; - } + IsCancelled = true, + Message = message + }; + } - public static JobResult SuccessWithMessage(string message) + public static JobResult SuccessWithMessage(string message) + { + return new JobResult { - return new JobResult - { - IsSuccess = true, - Message = message - }; - } + IsSuccess = true, + Message = message + }; + } - public static JobResult FailedWithMessage(string message) + public static JobResult FailedWithMessage(string message) + { + return new JobResult { - return new JobResult - { - IsSuccess = false, - Message = message - }; - } + IsSuccess = false, + Message = message + }; } +} - public static class JobResultExtensions +public static class JobResultExtensions +{ + public static void LogJobResult(this ILogger logger, JobResult result, string jobName) { - public static void LogJobResult(this ILogger logger, JobResult result, string jobName) + if (result == null) { - if (result == null) - { - if (logger.IsEnabled(LogLevel.Error)) - logger.LogError("Null job run result for {JobName}.", jobName); - - return; - } + if (logger.IsEnabled(LogLevel.Error)) + logger.LogError("Null job run result for {JobName}.", jobName); - if (result.IsCancelled) - logger.LogWarning(result.Error, "Job run {JobName} cancelled: {Message}", jobName, result.Message); - else if (!result.IsSuccess) - logger.LogError(result.Error, "Job run {JobName} failed: {Message}", jobName, result.Message); - else if (!String.IsNullOrEmpty(result.Message)) - logger.LogInformation("Job run {JobName} succeeded: {Message}", jobName, result.Message); - else if (logger.IsEnabled(LogLevel.Debug)) - logger.LogDebug("Job run {JobName} succeeded.", jobName); + return; } + + if (result.IsCancelled) + logger.LogWarning(result.Error, "Job run {JobName} cancelled: {Message}", jobName, result.Message); + else if (!result.IsSuccess) + logger.LogError(result.Error, "Job run {JobName} failed: {Message}", jobName, result.Message); + else if (!String.IsNullOrEmpty(result.Message)) + logger.LogInformation("Job run {JobName} succeeded: {Message}", jobName, result.Message); + else if (logger.IsEnabled(LogLevel.Debug)) + logger.LogDebug("Job run {JobName} succeeded.", jobName); } } diff --git a/src/Foundatio/Jobs/JobRunner.cs b/src/Foundatio/Jobs/JobRunner.cs index 25472215e..d40e79b31 100644 --- a/src/Foundatio/Jobs/JobRunner.cs +++ b/src/Foundatio/Jobs/JobRunner.cs @@ -8,258 +8,257 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -namespace Foundatio.Jobs +namespace Foundatio.Jobs; + +public class JobRunner { - public class JobRunner + private readonly ILogger _logger; + private string _jobName; + private readonly JobOptions _options; + + public JobRunner(JobOptions options, ILoggerFactory loggerFactory = null) { - private readonly ILogger _logger; - private string _jobName; - private readonly JobOptions _options; + _logger = loggerFactory?.CreateLogger() ?? NullLogger.Instance; + _options = options; + } - public JobRunner(JobOptions options, ILoggerFactory loggerFactory = null) + public JobRunner(IJob instance, ILoggerFactory loggerFactory = null, TimeSpan? initialDelay = null, int instanceCount = 1, bool runContinuous = true, int iterationLimit = -1, TimeSpan? interval = null) + : this(new JobOptions { - _logger = loggerFactory?.CreateLogger() ?? NullLogger.Instance; - _options = options; - } + JobFactory = () => instance, + InitialDelay = initialDelay, + InstanceCount = instanceCount, + IterationLimit = iterationLimit, + RunContinuous = runContinuous, + Interval = interval + }, loggerFactory) + { + } - public JobRunner(IJob instance, ILoggerFactory loggerFactory = null, TimeSpan? initialDelay = null, int instanceCount = 1, bool runContinuous = true, int iterationLimit = -1, TimeSpan? interval = null) - : this(new JobOptions - { - JobFactory = () => instance, - InitialDelay = initialDelay, - InstanceCount = instanceCount, - IterationLimit = iterationLimit, - RunContinuous = runContinuous, - Interval = interval - }, loggerFactory) + public JobRunner(Func jobFactory, ILoggerFactory loggerFactory = null, TimeSpan? initialDelay = null, int instanceCount = 1, bool runContinuous = true, int iterationLimit = -1, TimeSpan? interval = null) + : this(new JobOptions { - } + JobFactory = jobFactory, + InitialDelay = initialDelay, + InstanceCount = instanceCount, + IterationLimit = iterationLimit, + RunContinuous = runContinuous, + Interval = interval + }, loggerFactory) + { } - public JobRunner(Func jobFactory, ILoggerFactory loggerFactory = null, TimeSpan? initialDelay = null, int instanceCount = 1, bool runContinuous = true, int iterationLimit = -1, TimeSpan? interval = null) - : this(new JobOptions - { - JobFactory = jobFactory, - InitialDelay = initialDelay, - InstanceCount = instanceCount, - IterationLimit = iterationLimit, - RunContinuous = runContinuous, - Interval = interval - }, loggerFactory) - { } - - public CancellationTokenSource CancellationTokenSource { get; private set; } - - public async Task RunInConsoleAsync() - { - int result; - try - { - CancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(GetShutdownCancellationToken(_logger)); - bool success = await RunAsync(CancellationTokenSource.Token).AnyContext(); - result = success ? 0 : -1; + public CancellationTokenSource CancellationTokenSource { get; private set; } - if (Debugger.IsAttached) - Console.ReadKey(); - } - catch (TaskCanceledException) - { - return 0; - } - catch (FileNotFoundException e) - { - if (_logger.IsEnabled(LogLevel.Error)) - _logger.LogError("{Message} ({FileName})", e.GetMessage(), e.FileName); + public async Task RunInConsoleAsync() + { + int result; + try + { + CancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(GetShutdownCancellationToken(_logger)); + bool success = await RunAsync(CancellationTokenSource.Token).AnyContext(); + result = success ? 0 : -1; - if (Debugger.IsAttached) - Console.ReadKey(); + if (Debugger.IsAttached) + Console.ReadKey(); + } + catch (TaskCanceledException) + { + return 0; + } + catch (FileNotFoundException e) + { + if (_logger.IsEnabled(LogLevel.Error)) + _logger.LogError("{Message} ({FileName})", e.GetMessage(), e.FileName); - return 1; - } - catch (Exception e) - { - if (_logger.IsEnabled(LogLevel.Error)) - _logger.LogError(e, "Job {JobName} error: {Message}", _jobName, e.GetMessage()); + if (Debugger.IsAttached) + Console.ReadKey(); - if (Debugger.IsAttached) - Console.ReadKey(); + return 1; + } + catch (Exception e) + { + if (_logger.IsEnabled(LogLevel.Error)) + _logger.LogError(e, "Job {JobName} error: {Message}", _jobName, e.GetMessage()); - return 1; - } + if (Debugger.IsAttached) + Console.ReadKey(); - return result; + return 1; } - public void RunInBackground(CancellationToken cancellationToken = default) + return result; + } + + public void RunInBackground(CancellationToken cancellationToken = default) + { + if (_options.InstanceCount == 1) { - if (_options.InstanceCount == 1) + _ = Task.Run(async () => { - _ = Task.Run(async () => + try { - try - { - await RunAsync(cancellationToken).AnyContext(); - } - catch (TaskCanceledException) - { - } - catch (Exception ex) - { - if (_logger.IsEnabled(LogLevel.Error)) - _logger.LogError(ex, "Error running job in background: {Message}", ex.Message); - throw; - } - }, cancellationToken); - } - else - { - var ignored = RunAsync(cancellationToken); - } + await RunAsync(cancellationToken).AnyContext(); + } + catch (TaskCanceledException) + { + } + catch (Exception ex) + { + if (_logger.IsEnabled(LogLevel.Error)) + _logger.LogError(ex, "Error running job in background: {Message}", ex.Message); + throw; + } + }, cancellationToken); + } + else + { + var ignored = RunAsync(cancellationToken); } + } - public async Task RunAsync(CancellationToken cancellationToken = default) + public async Task RunAsync(CancellationToken cancellationToken = default) + { + if (_options.JobFactory == null) { - if (_options.JobFactory == null) - { - _logger.LogError("JobFactory must be specified"); - return false; - } + _logger.LogError("JobFactory must be specified"); + return false; + } - IJob job = null; - try - { - job = _options.JobFactory(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error creating job instance from JobFactory"); - return false; - } + IJob job = null; + try + { + job = _options.JobFactory(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating job instance from JobFactory"); + return false; + } - if (job == null) - { - _logger.LogError("JobFactory returned null job instance"); - return false; - } + if (job == null) + { + _logger.LogError("JobFactory returned null job instance"); + return false; + } + + _jobName = TypeHelper.GetTypeDisplayName(job.GetType()); + using (_logger.BeginScope(s => s.Property("job", _jobName))) + { + if (_logger.IsEnabled(LogLevel.Information)) + _logger.LogInformation("Starting job type {JobName} on machine {MachineName}...", _jobName, Environment.MachineName); - _jobName = TypeHelper.GetTypeDisplayName(job.GetType()); - using (_logger.BeginScope(s => s.Property("job", _jobName))) + var jobLifetime = job as IAsyncLifetime; + if (jobLifetime != null) { if (_logger.IsEnabled(LogLevel.Information)) - _logger.LogInformation("Starting job type {JobName} on machine {MachineName}...", _jobName, Environment.MachineName); + _logger.LogInformation("Initializing job lifetime {JobName} on machine {MachineName}...", _jobName, Environment.MachineName); + await jobLifetime.InitializeAsync().AnyContext(); + if (_logger.IsEnabled(LogLevel.Information)) + _logger.LogInformation("Done initializing job lifetime {JobName} on machine {MachineName}.", _jobName, Environment.MachineName); + } - var jobLifetime = job as IAsyncLifetime; - if (jobLifetime != null) - { - if (_logger.IsEnabled(LogLevel.Information)) - _logger.LogInformation("Initializing job lifetime {JobName} on machine {MachineName}...", _jobName, Environment.MachineName); - await jobLifetime.InitializeAsync().AnyContext(); - if (_logger.IsEnabled(LogLevel.Information)) - _logger.LogInformation("Done initializing job lifetime {JobName} on machine {MachineName}.", _jobName, Environment.MachineName); - } + try + { + if (_options.InitialDelay.HasValue && _options.InitialDelay.Value > TimeSpan.Zero) + await SystemClock.SleepAsync(_options.InitialDelay.Value, cancellationToken).AnyContext(); - try + if (_options.RunContinuous && _options.InstanceCount > 1) { - if (_options.InitialDelay.HasValue && _options.InitialDelay.Value > TimeSpan.Zero) - await SystemClock.SleepAsync(_options.InitialDelay.Value, cancellationToken).AnyContext(); - - if (_options.RunContinuous && _options.InstanceCount > 1) + var tasks = new List(_options.InstanceCount); + for (int i = 0; i < _options.InstanceCount; i++) { - var tasks = new List(_options.InstanceCount); - for (int i = 0; i < _options.InstanceCount; i++) + tasks.Add(Task.Run(async () => { - tasks.Add(Task.Run(async () => + try { - try - { - var jobInstance = _options.JobFactory(); - await jobInstance.RunContinuousAsync(_options.Interval, _options.IterationLimit, cancellationToken).AnyContext(); - } - catch (TaskCanceledException) - { - } - catch (Exception ex) - { - if (_logger.IsEnabled(LogLevel.Error)) - _logger.LogError(ex, "Error running job instance: {Message}", ex.Message); - throw; - } - }, cancellationToken)); - } - - await Task.WhenAll(tasks).AnyContext(); - } - else if (_options.RunContinuous && _options.InstanceCount == 1) - { - await job.RunContinuousAsync(_options.Interval, _options.IterationLimit, cancellationToken).AnyContext(); + var jobInstance = _options.JobFactory(); + await jobInstance.RunContinuousAsync(_options.Interval, _options.IterationLimit, cancellationToken).AnyContext(); + } + catch (TaskCanceledException) + { + } + catch (Exception ex) + { + if (_logger.IsEnabled(LogLevel.Error)) + _logger.LogError(ex, "Error running job instance: {Message}", ex.Message); + throw; + } + }, cancellationToken)); } - else - { - var result = await job.TryRunAsync(cancellationToken).AnyContext(); - _logger.LogJobResult(result, _jobName); - return result.IsSuccess; - } + await Task.WhenAll(tasks).AnyContext(); } - finally + else if (_options.RunContinuous && _options.InstanceCount == 1) { - var jobDisposable = job as IAsyncDisposable; - if (jobDisposable != null) - { - if (_logger.IsEnabled(LogLevel.Information)) - _logger.LogInformation("Disposing job lifetime {JobName} on machine {MachineName}...", _jobName, Environment.MachineName); - await jobDisposable.DisposeAsync().AnyContext(); - if (_logger.IsEnabled(LogLevel.Information)) - _logger.LogInformation("Done disposing job lifetime {JobName} on machine {MachineName}.", _jobName, Environment.MachineName); - } + await job.RunContinuousAsync(_options.Interval, _options.IterationLimit, cancellationToken).AnyContext(); } - } + else + { + var result = await job.TryRunAsync(cancellationToken).AnyContext(); + _logger.LogJobResult(result, _jobName); - return true; + return result.IsSuccess; + } + } + finally + { + var jobDisposable = job as IAsyncDisposable; + if (jobDisposable != null) + { + if (_logger.IsEnabled(LogLevel.Information)) + _logger.LogInformation("Disposing job lifetime {JobName} on machine {MachineName}...", _jobName, Environment.MachineName); + await jobDisposable.DisposeAsync().AnyContext(); + if (_logger.IsEnabled(LogLevel.Information)) + _logger.LogInformation("Done disposing job lifetime {JobName} on machine {MachineName}.", _jobName, Environment.MachineName); + } + } } - private static CancellationTokenSource _jobShutdownCancellationTokenSource; - private static readonly object _lock = new(); - public static CancellationToken GetShutdownCancellationToken(ILogger logger = null) + return true; + } + + private static CancellationTokenSource _jobShutdownCancellationTokenSource; + private static readonly object _lock = new(); + public static CancellationToken GetShutdownCancellationToken(ILogger logger = null) + { + if (_jobShutdownCancellationTokenSource != null) + return _jobShutdownCancellationTokenSource.Token; + + lock (_lock) { if (_jobShutdownCancellationTokenSource != null) return _jobShutdownCancellationTokenSource.Token; - lock (_lock) + _jobShutdownCancellationTokenSource = new CancellationTokenSource(); + Console.CancelKeyPress += (sender, args) => { - if (_jobShutdownCancellationTokenSource != null) - return _jobShutdownCancellationTokenSource.Token; + _jobShutdownCancellationTokenSource.Cancel(); + if (logger != null & logger.IsEnabled(LogLevel.Information)) + logger.LogInformation("Job shutdown event signaled: {SpecialKey}", args.SpecialKey); + args.Cancel = true; + }; - _jobShutdownCancellationTokenSource = new CancellationTokenSource(); - Console.CancelKeyPress += (sender, args) => - { - _jobShutdownCancellationTokenSource.Cancel(); - if (logger != null & logger.IsEnabled(LogLevel.Information)) - logger.LogInformation("Job shutdown event signaled: {SpecialKey}", args.SpecialKey); - args.Cancel = true; - }; - - string webJobsShutdownFile = Environment.GetEnvironmentVariable("WEBJOBS_SHUTDOWN_FILE"); - if (String.IsNullOrEmpty(webJobsShutdownFile)) - return _jobShutdownCancellationTokenSource.Token; + string webJobsShutdownFile = Environment.GetEnvironmentVariable("WEBJOBS_SHUTDOWN_FILE"); + if (String.IsNullOrEmpty(webJobsShutdownFile)) + return _jobShutdownCancellationTokenSource.Token; - var handler = new FileSystemEventHandler((s, e) => - { - if (e.FullPath.IndexOf(Path.GetFileName(webJobsShutdownFile), StringComparison.OrdinalIgnoreCase) < 0) - return; + var handler = new FileSystemEventHandler((s, e) => + { + if (e.FullPath.IndexOf(Path.GetFileName(webJobsShutdownFile), StringComparison.OrdinalIgnoreCase) < 0) + return; - _jobShutdownCancellationTokenSource.Cancel(); - logger?.LogInformation("Job shutdown signaled"); - }); + _jobShutdownCancellationTokenSource.Cancel(); + logger?.LogInformation("Job shutdown signaled"); + }); - var watcher = new FileSystemWatcher(Path.GetDirectoryName(webJobsShutdownFile)); - watcher.Created += handler; - watcher.Changed += handler; - watcher.NotifyFilter = NotifyFilters.CreationTime | NotifyFilters.FileName | NotifyFilters.LastWrite; - watcher.IncludeSubdirectories = false; - watcher.EnableRaisingEvents = true; + var watcher = new FileSystemWatcher(Path.GetDirectoryName(webJobsShutdownFile)); + watcher.Created += handler; + watcher.Changed += handler; + watcher.NotifyFilter = NotifyFilters.CreationTime | NotifyFilters.FileName | NotifyFilters.LastWrite; + watcher.IncludeSubdirectories = false; + watcher.EnableRaisingEvents = true; - return _jobShutdownCancellationTokenSource.Token; - } + return _jobShutdownCancellationTokenSource.Token; } } } diff --git a/src/Foundatio/Jobs/JobWithLockBase.cs b/src/Foundatio/Jobs/JobWithLockBase.cs index a95e612ee..d6f0a8b9b 100644 --- a/src/Foundatio/Jobs/JobWithLockBase.cs +++ b/src/Foundatio/Jobs/JobWithLockBase.cs @@ -6,41 +6,40 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -namespace Foundatio.Jobs +namespace Foundatio.Jobs; + +public abstract class JobWithLockBase : IJob, IHaveLogger { - public abstract class JobWithLockBase : IJob, IHaveLogger + protected readonly ILogger _logger; + + public JobWithLockBase(ILoggerFactory loggerFactory = null) { - protected readonly ILogger _logger; + _logger = loggerFactory?.CreateLogger(GetType()) ?? NullLogger.Instance; + } - public JobWithLockBase(ILoggerFactory loggerFactory = null) + public string JobId { get; } = Guid.NewGuid().ToString("N").Substring(0, 10); + ILogger IHaveLogger.Logger => _logger; + + public async virtual Task RunAsync(CancellationToken cancellationToken = default) + { + var lockValue = await GetLockAsync(cancellationToken).AnyContext(); + if (lockValue == null) { - _logger = loggerFactory?.CreateLogger(GetType()) ?? NullLogger.Instance; + _logger.LogTrace("Unable to acquire job lock"); + return JobResult.Success; } - public string JobId { get; } = Guid.NewGuid().ToString("N").Substring(0, 10); - ILogger IHaveLogger.Logger => _logger; - - public async virtual Task RunAsync(CancellationToken cancellationToken = default) + try + { + return await RunInternalAsync(new JobContext(cancellationToken, lockValue)).AnyContext(); + } + finally { - var lockValue = await GetLockAsync(cancellationToken).AnyContext(); - if (lockValue == null) - { - _logger.LogTrace("Unable to acquire job lock"); - return JobResult.Success; - } - - try - { - return await RunInternalAsync(new JobContext(cancellationToken, lockValue)).AnyContext(); - } - finally - { - await lockValue.ReleaseAsync().AnyContext(); - } + await lockValue.ReleaseAsync().AnyContext(); } + } - protected abstract Task RunInternalAsync(JobContext context); + protected abstract Task RunInternalAsync(JobContext context); - protected abstract Task GetLockAsync(CancellationToken cancellationToken = default); - } + protected abstract Task GetLockAsync(CancellationToken cancellationToken = default); } diff --git a/src/Foundatio/Jobs/QueueEntryContext.cs b/src/Foundatio/Jobs/QueueEntryContext.cs index f0ed993f1..04ba268a8 100644 --- a/src/Foundatio/Jobs/QueueEntryContext.cs +++ b/src/Foundatio/Jobs/QueueEntryContext.cs @@ -4,23 +4,22 @@ using Foundatio.Queues; using Foundatio.Utility; -namespace Foundatio.Jobs +namespace Foundatio.Jobs; + +public class QueueEntryContext : JobContext where T : class { - public class QueueEntryContext : JobContext where T : class + public QueueEntryContext(IQueueEntry queueEntry, ILock queueEntryLock, CancellationToken cancellationToken = default) : base(cancellationToken, queueEntryLock) { - public QueueEntryContext(IQueueEntry queueEntry, ILock queueEntryLock, CancellationToken cancellationToken = default) : base(cancellationToken, queueEntryLock) - { - QueueEntry = queueEntry; - } + QueueEntry = queueEntry; + } - public IQueueEntry QueueEntry { get; private set; } + public IQueueEntry QueueEntry { get; private set; } - public override async Task RenewLockAsync() - { - if (QueueEntry != null) - await QueueEntry.RenewLockAsync().AnyContext(); + public override async Task RenewLockAsync() + { + if (QueueEntry != null) + await QueueEntry.RenewLockAsync().AnyContext(); - await base.RenewLockAsync().AnyContext(); - } + await base.RenewLockAsync().AnyContext(); } } diff --git a/src/Foundatio/Jobs/QueueJobBase.cs b/src/Foundatio/Jobs/QueueJobBase.cs index 3a332fc55..ee2653fd3 100644 --- a/src/Foundatio/Jobs/QueueJobBase.cs +++ b/src/Foundatio/Jobs/QueueJobBase.cs @@ -9,182 +9,181 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -namespace Foundatio.Jobs +namespace Foundatio.Jobs; + +public abstract class QueueJobBase : IQueueJob, IHaveLogger where T : class { - public abstract class QueueJobBase : IQueueJob, IHaveLogger where T : class + protected readonly ILogger _logger; + protected readonly Lazy> _queue; + protected readonly string _queueEntryName = typeof(T).Name; + + public QueueJobBase(Lazy> queue, ILoggerFactory loggerFactory = null) { - protected readonly ILogger _logger; - protected readonly Lazy> _queue; - protected readonly string _queueEntryName = typeof(T).Name; + _queue = queue; + _logger = loggerFactory?.CreateLogger(GetType()) ?? NullLogger.Instance; + AutoComplete = true; + } - public QueueJobBase(Lazy> queue, ILoggerFactory loggerFactory = null) - { - _queue = queue; - _logger = loggerFactory?.CreateLogger(GetType()) ?? NullLogger.Instance; - AutoComplete = true; - } + public QueueJobBase(IQueue queue, ILoggerFactory loggerFactory = null) : this(new Lazy>(() => queue), loggerFactory) { } - public QueueJobBase(IQueue queue, ILoggerFactory loggerFactory = null) : this(new Lazy>(() => queue), loggerFactory) { } + protected bool AutoComplete { get; set; } + public string JobId { get; } = Guid.NewGuid().ToString("N").Substring(0, 10); + IQueue IQueueJob.Queue => _queue.Value; + ILogger IHaveLogger.Logger => _logger; - protected bool AutoComplete { get; set; } - public string JobId { get; } = Guid.NewGuid().ToString("N").Substring(0, 10); - IQueue IQueueJob.Queue => _queue.Value; - ILogger IHaveLogger.Logger => _logger; + public virtual async Task RunAsync(CancellationToken cancellationToken = default) + { + IQueueEntry queueEntry; - public virtual async Task RunAsync(CancellationToken cancellationToken = default) + using (var timeoutCancellationTokenSource = new CancellationTokenSource(30000)) + using (var linkedCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCancellationTokenSource.Token)) { - IQueueEntry queueEntry; - - using (var timeoutCancellationTokenSource = new CancellationTokenSource(30000)) - using (var linkedCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCancellationTokenSource.Token)) + try { - try - { - queueEntry = await _queue.Value.DequeueAsync(linkedCancellationToken.Token).AnyContext(); - } - catch (Exception ex) - { - return JobResult.FromException(ex, $"Error trying to dequeue message: {ex.Message}"); - } + queueEntry = await _queue.Value.DequeueAsync(linkedCancellationToken.Token).AnyContext(); + } + catch (Exception ex) + { + return JobResult.FromException(ex, $"Error trying to dequeue message: {ex.Message}"); } - - return await ProcessAsync(queueEntry, cancellationToken).AnyContext(); } - public async Task ProcessAsync(IQueueEntry queueEntry, CancellationToken cancellationToken) - { - if (queueEntry == null) - return JobResult.Success; + return await ProcessAsync(queueEntry, cancellationToken).AnyContext(); + } - using var activity = StartProcessQueueEntryActivity(queueEntry); - using var _ = _logger.BeginScope(s => s - .Property("JobId", JobId) - .Property("QueueEntryId", queueEntry.Id) - .PropertyIf("CorrelationId", queueEntry.CorrelationId, !String.IsNullOrEmpty(queueEntry.CorrelationId)) - .Property("QueueEntryName", _queueEntryName)); + public async Task ProcessAsync(IQueueEntry queueEntry, CancellationToken cancellationToken) + { + if (queueEntry == null) + return JobResult.Success; - _logger.LogInformation("Processing queue entry: id={QueueEntryId} type={QueueEntryName} attempt={QueueEntryAttempt}", queueEntry.Id, _queueEntryName, queueEntry.Attempts); + using var activity = StartProcessQueueEntryActivity(queueEntry); + using var _ = _logger.BeginScope(s => s + .Property("JobId", JobId) + .Property("QueueEntryId", queueEntry.Id) + .PropertyIf("CorrelationId", queueEntry.CorrelationId, !String.IsNullOrEmpty(queueEntry.CorrelationId)) + .Property("QueueEntryName", _queueEntryName)); - if (cancellationToken.IsCancellationRequested) - { - if (_logger.IsEnabled(LogLevel.Information)) - _logger.LogInformation("Job was cancelled. Abandoning {QueueEntryName} queue entry: {Id}", _queueEntryName, queueEntry.Id); + _logger.LogInformation("Processing queue entry: id={QueueEntryId} type={QueueEntryName} attempt={QueueEntryAttempt}", queueEntry.Id, _queueEntryName, queueEntry.Attempts); - await queueEntry.AbandonAsync().AnyContext(); - return JobResult.CancelledWithMessage($"Abandoning {_queueEntryName} queue entry: {queueEntry.Id}"); - } + if (cancellationToken.IsCancellationRequested) + { + if (_logger.IsEnabled(LogLevel.Information)) + _logger.LogInformation("Job was cancelled. Abandoning {QueueEntryName} queue entry: {Id}", _queueEntryName, queueEntry.Id); - var lockValue = await GetQueueEntryLockAsync(queueEntry, cancellationToken).AnyContext(); - if (lockValue == null) - { - await queueEntry.AbandonAsync().AnyContext(); - _logger.LogTrace("Unable to acquire queue entry lock"); - return JobResult.Success; - } + await queueEntry.AbandonAsync().AnyContext(); + return JobResult.CancelledWithMessage($"Abandoning {_queueEntryName} queue entry: {queueEntry.Id}"); + } - bool isTraceLogLevelEnabled = _logger.IsEnabled(LogLevel.Trace); - try - { - LogProcessingQueueEntry(queueEntry); - var result = await ProcessQueueEntryAsync(new QueueEntryContext(queueEntry, lockValue, cancellationToken)).AnyContext(); - - if (!AutoComplete || queueEntry.IsCompleted || queueEntry.IsAbandoned) - return result; - - if (result.IsSuccess) - { - await queueEntry.CompleteAsync().AnyContext(); - LogAutoCompletedQueueEntry(queueEntry); - } - else - { - if (result.Error != null || result.Message != null) - _logger.LogError(result.Error, "{QueueEntryName} queue entry {Id} returned an unsuccessful response: {Message}", _queueEntryName, queueEntry.Id, result.Message ?? result.Error?.Message); - - if (isTraceLogLevelEnabled) - _logger.LogTrace("Processing was not successful. Auto Abandoning {QueueEntryName} queue entry: {Id}", _queueEntryName, queueEntry.Id); - await queueEntry.AbandonAsync().AnyContext(); - if (_logger.IsEnabled(LogLevel.Warning)) - _logger.LogWarning("Auto abandoned {QueueEntryName} queue entry: {Id}", _queueEntryName, queueEntry.Id); - } + var lockValue = await GetQueueEntryLockAsync(queueEntry, cancellationToken).AnyContext(); + if (lockValue == null) + { + await queueEntry.AbandonAsync().AnyContext(); + _logger.LogTrace("Unable to acquire queue entry lock"); + return JobResult.Success; + } - return result; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error processing {QueueEntryName} queue entry: {Id}", _queueEntryName, queueEntry.Id); + bool isTraceLogLevelEnabled = _logger.IsEnabled(LogLevel.Trace); + try + { + LogProcessingQueueEntry(queueEntry); + var result = await ProcessQueueEntryAsync(new QueueEntryContext(queueEntry, lockValue, cancellationToken)).AnyContext(); - if (!queueEntry.IsCompleted && !queueEntry.IsAbandoned) - await queueEntry.AbandonAsync().AnyContext(); + if (!AutoComplete || queueEntry.IsCompleted || queueEntry.IsAbandoned) + return result; - throw; + if (result.IsSuccess) + { + await queueEntry.CompleteAsync().AnyContext(); + LogAutoCompletedQueueEntry(queueEntry); } - finally + else { + if (result.Error != null || result.Message != null) + _logger.LogError(result.Error, "{QueueEntryName} queue entry {Id} returned an unsuccessful response: {Message}", _queueEntryName, queueEntry.Id, result.Message ?? result.Error?.Message); + if (isTraceLogLevelEnabled) - _logger.LogTrace("Releasing Lock for {QueueEntryName} queue entry: {Id}", _queueEntryName, queueEntry.Id); - await lockValue.ReleaseAsync().AnyContext(); - if (isTraceLogLevelEnabled) - _logger.LogTrace("Released Lock for {QueueEntryName} queue entry: {Id}", _queueEntryName, queueEntry.Id); + _logger.LogTrace("Processing was not successful. Auto Abandoning {QueueEntryName} queue entry: {Id}", _queueEntryName, queueEntry.Id); + await queueEntry.AbandonAsync().AnyContext(); + if (_logger.IsEnabled(LogLevel.Warning)) + _logger.LogWarning("Auto abandoned {QueueEntryName} queue entry: {Id}", _queueEntryName, queueEntry.Id); } + + return result; } + catch (Exception ex) + { + _logger.LogError(ex, "Error processing {QueueEntryName} queue entry: {Id}", _queueEntryName, queueEntry.Id); + + if (!queueEntry.IsCompleted && !queueEntry.IsAbandoned) + await queueEntry.AbandonAsync().AnyContext(); - protected virtual Activity StartProcessQueueEntryActivity(IQueueEntry entry) + throw; + } + finally { - var activity = FoundatioDiagnostics.ActivitySource.StartActivity("ProcessQueueEntry", ActivityKind.Server, entry.CorrelationId); + if (isTraceLogLevelEnabled) + _logger.LogTrace("Releasing Lock for {QueueEntryName} queue entry: {Id}", _queueEntryName, queueEntry.Id); + await lockValue.ReleaseAsync().AnyContext(); + if (isTraceLogLevelEnabled) + _logger.LogTrace("Released Lock for {QueueEntryName} queue entry: {Id}", _queueEntryName, queueEntry.Id); + } + } - if (activity == null) - return activity; + protected virtual Activity StartProcessQueueEntryActivity(IQueueEntry entry) + { + var activity = FoundatioDiagnostics.ActivitySource.StartActivity("ProcessQueueEntry", ActivityKind.Server, entry.CorrelationId); - if (entry.Properties != null && entry.Properties.TryGetValue("TraceState", out var traceState)) - activity.TraceStateString = traceState.ToString(); + if (activity == null) + return activity; - activity.DisplayName = $"Queue: {entry.EntryType.Name}"; + if (entry.Properties != null && entry.Properties.TryGetValue("TraceState", out var traceState)) + activity.TraceStateString = traceState.ToString(); - EnrichProcessQueueEntryActivity(activity, entry); + activity.DisplayName = $"Queue: {entry.EntryType.Name}"; - return activity; - } + EnrichProcessQueueEntryActivity(activity, entry); - protected virtual void EnrichProcessQueueEntryActivity(Activity activity, IQueueEntry entry) - { - if (!activity.IsAllDataRequested) - return; + return activity; + } - activity.AddTag("EntryType", entry.EntryType.FullName); - activity.AddTag("Id", entry.Id); - activity.AddTag("CorrelationId", entry.CorrelationId); + protected virtual void EnrichProcessQueueEntryActivity(Activity activity, IQueueEntry entry) + { + if (!activity.IsAllDataRequested) + return; - if (entry.Properties == null || entry.Properties.Count <= 0) - return; + activity.AddTag("EntryType", entry.EntryType.FullName); + activity.AddTag("Id", entry.Id); + activity.AddTag("CorrelationId", entry.CorrelationId); - foreach (var p in entry.Properties) - { - if (p.Key != "TraceState") - activity.AddTag(p.Key, p.Value); - } - } + if (entry.Properties == null || entry.Properties.Count <= 0) + return; - protected virtual void LogProcessingQueueEntry(IQueueEntry queueEntry) + foreach (var p in entry.Properties) { - if (_logger.IsEnabled(LogLevel.Information)) - _logger.LogInformation("Processing {QueueEntryName} queue entry: {Id}", _queueEntryName, queueEntry.Id); + if (p.Key != "TraceState") + activity.AddTag(p.Key, p.Value); } + } - protected virtual void LogAutoCompletedQueueEntry(IQueueEntry queueEntry) - { - if (_logger.IsEnabled(LogLevel.Information)) - _logger.LogInformation("Auto completed {QueueEntryName} queue entry: {Id}", _queueEntryName, queueEntry.Id); - } + protected virtual void LogProcessingQueueEntry(IQueueEntry queueEntry) + { + if (_logger.IsEnabled(LogLevel.Information)) + _logger.LogInformation("Processing {QueueEntryName} queue entry: {Id}", _queueEntryName, queueEntry.Id); + } + + protected virtual void LogAutoCompletedQueueEntry(IQueueEntry queueEntry) + { + if (_logger.IsEnabled(LogLevel.Information)) + _logger.LogInformation("Auto completed {QueueEntryName} queue entry: {Id}", _queueEntryName, queueEntry.Id); + } - protected abstract Task ProcessQueueEntryAsync(QueueEntryContext context); + protected abstract Task ProcessQueueEntryAsync(QueueEntryContext context); - protected virtual Task GetQueueEntryLockAsync(IQueueEntry queueEntry, CancellationToken cancellationToken = default) - { - if (_logger.IsEnabled(LogLevel.Trace)) - _logger.LogTrace("Returning Empty Lock for {QueueEntryName} queue entry: {Id}", _queueEntryName, queueEntry.Id); + protected virtual Task GetQueueEntryLockAsync(IQueueEntry queueEntry, CancellationToken cancellationToken = default) + { + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.LogTrace("Returning Empty Lock for {QueueEntryName} queue entry: {Id}", _queueEntryName, queueEntry.Id); - return Task.FromResult(Disposable.EmptyLock); - } + return Task.FromResult(Disposable.EmptyLock); } } diff --git a/src/Foundatio/Jobs/WorkItemJob/WorkItemContext.cs b/src/Foundatio/Jobs/WorkItemJob/WorkItemContext.cs index 33739cb93..d62e45c59 100644 --- a/src/Foundatio/Jobs/WorkItemJob/WorkItemContext.cs +++ b/src/Foundatio/Jobs/WorkItemJob/WorkItemContext.cs @@ -3,43 +3,42 @@ using System.Threading.Tasks; using Foundatio.Lock; -namespace Foundatio.Jobs +namespace Foundatio.Jobs; + +public class WorkItemContext { - public class WorkItemContext + private readonly Func _progressCallback; + + public WorkItemContext(object data, string jobId, ILock workItemLock, CancellationToken cancellationToken, Func progressCallback) + { + Data = data; + JobId = jobId; + WorkItemLock = workItemLock; + CancellationToken = cancellationToken; + _progressCallback = progressCallback; + } + + public object Data { get; private set; } + public string JobId { get; private set; } + public ILock WorkItemLock { get; private set; } + public JobResult Result { get; set; } = JobResult.Success; + public CancellationToken CancellationToken { get; private set; } + + public Task ReportProgressAsync(int progress, string message = null) + { + return _progressCallback(progress, message); + } + + public Task RenewLockAsync() + { + if (WorkItemLock != null) + return WorkItemLock.RenewAsync(); + + return Task.CompletedTask; + } + + public T GetData() where T : class { - private readonly Func _progressCallback; - - public WorkItemContext(object data, string jobId, ILock workItemLock, CancellationToken cancellationToken, Func progressCallback) - { - Data = data; - JobId = jobId; - WorkItemLock = workItemLock; - CancellationToken = cancellationToken; - _progressCallback = progressCallback; - } - - public object Data { get; private set; } - public string JobId { get; private set; } - public ILock WorkItemLock { get; private set; } - public JobResult Result { get; set; } = JobResult.Success; - public CancellationToken CancellationToken { get; private set; } - - public Task ReportProgressAsync(int progress, string message = null) - { - return _progressCallback(progress, message); - } - - public Task RenewLockAsync() - { - if (WorkItemLock != null) - return WorkItemLock.RenewAsync(); - - return Task.CompletedTask; - } - - public T GetData() where T : class - { - return Data as T; - } + return Data as T; } } diff --git a/src/Foundatio/Jobs/WorkItemJob/WorkItemData.cs b/src/Foundatio/Jobs/WorkItemJob/WorkItemData.cs index ab3ceeb2a..003798113 100644 --- a/src/Foundatio/Jobs/WorkItemJob/WorkItemData.cs +++ b/src/Foundatio/Jobs/WorkItemJob/WorkItemData.cs @@ -2,15 +2,14 @@ using Foundatio.Metrics; using Foundatio.Queues; -namespace Foundatio.Jobs +namespace Foundatio.Jobs; + +public class WorkItemData : IHaveSubMetricName, IHaveUniqueIdentifier { - public class WorkItemData : IHaveSubMetricName, IHaveUniqueIdentifier - { - public string WorkItemId { get; set; } - public string Type { get; set; } - public byte[] Data { get; set; } - public bool SendProgressReports { get; set; } - public string UniqueIdentifier { get; set; } - public string SubMetricName { get; set; } - } + public string WorkItemId { get; set; } + public string Type { get; set; } + public byte[] Data { get; set; } + public bool SendProgressReports { get; set; } + public string UniqueIdentifier { get; set; } + public string SubMetricName { get; set; } } diff --git a/src/Foundatio/Jobs/WorkItemJob/WorkItemHandlers.cs b/src/Foundatio/Jobs/WorkItemJob/WorkItemHandlers.cs index 389aff5c2..30c8944ec 100644 --- a/src/Foundatio/Jobs/WorkItemJob/WorkItemHandlers.cs +++ b/src/Foundatio/Jobs/WorkItemJob/WorkItemHandlers.cs @@ -8,125 +8,124 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -namespace Foundatio.Jobs +namespace Foundatio.Jobs; + +public class WorkItemHandlers +{ + private readonly ConcurrentDictionary> _handlers; + + public WorkItemHandlers() + { + _handlers = new ConcurrentDictionary>(); + } + + public void Register(IWorkItemHandler handler) + { + _handlers.TryAdd(typeof(T), new Lazy(() => handler)); + } + + public void Register(Func handler) + { + _handlers.TryAdd(typeof(T), new Lazy(handler)); + } + + public void Register(Func handler, ILogger logger = null, Action, Type, object> logProcessingWorkItem = null, Action, Type, object> logAutoCompletedWorkItem = null) where T : class + { + _handlers.TryAdd(typeof(T), new Lazy(() => new DelegateWorkItemHandler(handler, logger, logProcessingWorkItem, logAutoCompletedWorkItem))); + } + + public IWorkItemHandler GetHandler(Type jobDataType) + { + if (!_handlers.TryGetValue(jobDataType, out var handler)) + return null; + + return handler.Value; + } +} + +public interface IWorkItemHandler +{ + Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = default); + Task HandleItemAsync(WorkItemContext context); + bool AutoRenewLockOnProgress { get; set; } + ILogger Log { get; set; } + void LogProcessingQueueEntry(IQueueEntry queueEntry, Type workItemDataType, object workItem); + void LogAutoCompletedQueueEntry(IQueueEntry queueEntry, Type workItemDataType, object workItem); +} + +public abstract class WorkItemHandlerBase : IWorkItemHandler { - public class WorkItemHandlers + public WorkItemHandlerBase(ILoggerFactory loggerFactory = null) { - private readonly ConcurrentDictionary> _handlers; - - public WorkItemHandlers() - { - _handlers = new ConcurrentDictionary>(); - } - - public void Register(IWorkItemHandler handler) - { - _handlers.TryAdd(typeof(T), new Lazy(() => handler)); - } - - public void Register(Func handler) - { - _handlers.TryAdd(typeof(T), new Lazy(handler)); - } - - public void Register(Func handler, ILogger logger = null, Action, Type, object> logProcessingWorkItem = null, Action, Type, object> logAutoCompletedWorkItem = null) where T : class - { - _handlers.TryAdd(typeof(T), new Lazy(() => new DelegateWorkItemHandler(handler, logger, logProcessingWorkItem, logAutoCompletedWorkItem))); - } - - public IWorkItemHandler GetHandler(Type jobDataType) - { - if (!_handlers.TryGetValue(jobDataType, out var handler)) - return null; - - return handler.Value; - } + Log = loggerFactory?.CreateLogger(GetType()) ?? NullLogger.Instance; } + public WorkItemHandlerBase(ILogger logger) + { + Log = logger ?? NullLogger.Instance; + } + + public virtual Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = default) + { + return Task.FromResult(Disposable.EmptyLock); + } + + public bool AutoRenewLockOnProgress { get; set; } + public ILogger Log { get; set; } - public interface IWorkItemHandler + public virtual void LogProcessingQueueEntry(IQueueEntry queueEntry, Type workItemDataType, object workItem) { - Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = default); - Task HandleItemAsync(WorkItemContext context); - bool AutoRenewLockOnProgress { get; set; } - ILogger Log { get; set; } - void LogProcessingQueueEntry(IQueueEntry queueEntry, Type workItemDataType, object workItem); - void LogAutoCompletedQueueEntry(IQueueEntry queueEntry, Type workItemDataType, object workItem); + if (Log.IsEnabled(LogLevel.Information)) + Log.LogInformation("Processing {TypeName} work item queue entry: {Id}.", workItemDataType.Name, queueEntry.Id); + } + + public virtual void LogAutoCompletedQueueEntry(IQueueEntry queueEntry, Type workItemDataType, object workItem) + { + if (Log.IsEnabled(LogLevel.Information)) + Log.LogInformation("Auto completed {TypeName} work item queue entry: {Id}.", workItemDataType.Name, queueEntry.Id); + } + + public abstract Task HandleItemAsync(WorkItemContext context); + + protected int CalculateProgress(long total, long completed, int startProgress = 0, int endProgress = 100) + { + return startProgress + (int)((100 * (double)completed / total) * (((double)endProgress - startProgress) / 100)); + } +} + +public class DelegateWorkItemHandler : WorkItemHandlerBase +{ + private readonly Func _handler; + private readonly Action, Type, object> _logProcessingWorkItem; + private readonly Action, Type, object> _logAutoCompletedWorkItem; + + public DelegateWorkItemHandler(Func handler, ILogger logger = null, Action, Type, object> logProcessingWorkItem = null, Action, Type, object> logAutoCompletedWorkItem = null) : base(logger) + { + _handler = handler; + _logProcessingWorkItem = logProcessingWorkItem; + _logAutoCompletedWorkItem = logAutoCompletedWorkItem; + } + + public override Task HandleItemAsync(WorkItemContext context) + { + if (_handler == null) + return Task.CompletedTask; + + return _handler(context); } - public abstract class WorkItemHandlerBase : IWorkItemHandler + public override void LogProcessingQueueEntry(IQueueEntry queueEntry, Type workItemDataType, object workItem) { - public WorkItemHandlerBase(ILoggerFactory loggerFactory = null) - { - Log = loggerFactory?.CreateLogger(GetType()) ?? NullLogger.Instance; - } - public WorkItemHandlerBase(ILogger logger) - { - Log = logger ?? NullLogger.Instance; - } - - public virtual Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = default) - { - return Task.FromResult(Disposable.EmptyLock); - } - - public bool AutoRenewLockOnProgress { get; set; } - public ILogger Log { get; set; } - - public virtual void LogProcessingQueueEntry(IQueueEntry queueEntry, Type workItemDataType, object workItem) - { - if (Log.IsEnabled(LogLevel.Information)) - Log.LogInformation("Processing {TypeName} work item queue entry: {Id}.", workItemDataType.Name, queueEntry.Id); - } - - public virtual void LogAutoCompletedQueueEntry(IQueueEntry queueEntry, Type workItemDataType, object workItem) - { - if (Log.IsEnabled(LogLevel.Information)) - Log.LogInformation("Auto completed {TypeName} work item queue entry: {Id}.", workItemDataType.Name, queueEntry.Id); - } - - public abstract Task HandleItemAsync(WorkItemContext context); - - protected int CalculateProgress(long total, long completed, int startProgress = 0, int endProgress = 100) - { - return startProgress + (int)((100 * (double)completed / total) * (((double)endProgress - startProgress) / 100)); - } + if (_logProcessingWorkItem != null) + _logProcessingWorkItem(queueEntry, workItemDataType, workItem); + else + base.LogProcessingQueueEntry(queueEntry, workItemDataType, workItem); } - public class DelegateWorkItemHandler : WorkItemHandlerBase + public override void LogAutoCompletedQueueEntry(IQueueEntry queueEntry, Type workItemDataType, object workItem) { - private readonly Func _handler; - private readonly Action, Type, object> _logProcessingWorkItem; - private readonly Action, Type, object> _logAutoCompletedWorkItem; - - public DelegateWorkItemHandler(Func handler, ILogger logger = null, Action, Type, object> logProcessingWorkItem = null, Action, Type, object> logAutoCompletedWorkItem = null) : base(logger) - { - _handler = handler; - _logProcessingWorkItem = logProcessingWorkItem; - _logAutoCompletedWorkItem = logAutoCompletedWorkItem; - } - - public override Task HandleItemAsync(WorkItemContext context) - { - if (_handler == null) - return Task.CompletedTask; - - return _handler(context); - } - - public override void LogProcessingQueueEntry(IQueueEntry queueEntry, Type workItemDataType, object workItem) - { - if (_logProcessingWorkItem != null) - _logProcessingWorkItem(queueEntry, workItemDataType, workItem); - else - base.LogProcessingQueueEntry(queueEntry, workItemDataType, workItem); - } - - public override void LogAutoCompletedQueueEntry(IQueueEntry queueEntry, Type workItemDataType, object workItem) - { - if (_logAutoCompletedWorkItem != null) - _logAutoCompletedWorkItem(queueEntry, workItemDataType, workItem); - else - base.LogAutoCompletedQueueEntry(queueEntry, workItemDataType, workItem); - } + if (_logAutoCompletedWorkItem != null) + _logAutoCompletedWorkItem(queueEntry, workItemDataType, workItem); + else + base.LogAutoCompletedQueueEntry(queueEntry, workItemDataType, workItem); } } diff --git a/src/Foundatio/Jobs/WorkItemJob/WorkItemJob.cs b/src/Foundatio/Jobs/WorkItemJob/WorkItemJob.cs index 6feb9393b..62c508b8a 100644 --- a/src/Foundatio/Jobs/WorkItemJob/WorkItemJob.cs +++ b/src/Foundatio/Jobs/WorkItemJob/WorkItemJob.cs @@ -10,257 +10,256 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -namespace Foundatio.Jobs +namespace Foundatio.Jobs; + +[Job(Description = "Processes adhoc work item queues entries")] +public class WorkItemJob : IQueueJob, IHaveLogger { - [Job(Description = "Processes adhoc work item queues entries")] - public class WorkItemJob : IQueueJob, IHaveLogger + protected readonly IMessagePublisher _publisher; + protected readonly WorkItemHandlers _handlers; + protected readonly IQueue _queue; + protected readonly ILogger _logger; + + public WorkItemJob(IQueue queue, IMessagePublisher publisher, WorkItemHandlers handlers, ILoggerFactory loggerFactory = null) + { + _publisher = publisher; + _handlers = handlers; + _queue = queue; + _logger = loggerFactory?.CreateLogger(GetType()) ?? NullLogger.Instance; + } + + public string JobId { get; } = Guid.NewGuid().ToString("N").Substring(0, 10); + IQueue IQueueJob.Queue => _queue; + ILogger IHaveLogger.Logger => _logger; + + public async virtual Task RunAsync(CancellationToken cancellationToken = default) { - protected readonly IMessagePublisher _publisher; - protected readonly WorkItemHandlers _handlers; - protected readonly IQueue _queue; - protected readonly ILogger _logger; + IQueueEntry queueEntry; + + using (var timeoutCancellationTokenSource = new CancellationTokenSource(30000)) + using (var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCancellationTokenSource.Token)) + { + try + { + queueEntry = await _queue.DequeueAsync(linkedCancellationTokenSource.Token).AnyContext(); + } + catch (Exception ex) + { + return JobResult.FromException(ex, $"Error trying to dequeue work item: {ex.Message}"); + } + } + + return await ProcessAsync(queueEntry, cancellationToken).AnyContext(); + } + + public async Task ProcessAsync(IQueueEntry queueEntry, CancellationToken cancellationToken) + { + if (queueEntry == null) + return JobResult.Success; + + if (cancellationToken.IsCancellationRequested) + { + await queueEntry.AbandonAsync().AnyContext(); + return JobResult.CancelledWithMessage($"Abandoning {queueEntry.Value.Type} work item: {queueEntry.Id}"); + } + + var workItemDataType = GetWorkItemType(queueEntry.Value.Type); + if (workItemDataType == null) + { + await queueEntry.AbandonAsync().AnyContext(); + return JobResult.FailedWithMessage($"Abandoning {queueEntry.Value.Type} work item: {queueEntry.Id}: Could not resolve work item data type"); + } + + using var activity = StartProcessWorkItemActivity(queueEntry, workItemDataType); + using var _ = _logger.BeginScope(s => s + .Property("JobId", JobId) + .Property("QueueEntryId", queueEntry.Id) + .PropertyIf("CorrelationId", queueEntry.CorrelationId, !String.IsNullOrEmpty(queueEntry.CorrelationId)) + .Property("QueueEntryName", workItemDataType.Name)); + + object workItemData; + try + { + workItemData = _queue.Serializer.Deserialize(queueEntry.Value.Data, workItemDataType); + } + catch (Exception ex) + { + await queueEntry.AbandonAsync().AnyContext(); + return JobResult.FromException(ex, $"Abandoning {queueEntry.Value.Type} work item: {queueEntry.Id}: Failed to parse {workItemDataType.Name} work item data"); + } - public WorkItemJob(IQueue queue, IMessagePublisher publisher, WorkItemHandlers handlers, ILoggerFactory loggerFactory = null) + var handler = _handlers.GetHandler(workItemDataType); + if (handler == null) { - _publisher = publisher; - _handlers = handlers; - _queue = queue; - _logger = loggerFactory?.CreateLogger(GetType()) ?? NullLogger.Instance; + await queueEntry.CompleteAsync().AnyContext(); + return JobResult.FailedWithMessage($"Completing {queueEntry.Value.Type} work item: {queueEntry.Id}: Handler for type {workItemDataType.Name} not registered"); } - public string JobId { get; } = Guid.NewGuid().ToString("N").Substring(0, 10); - IQueue IQueueJob.Queue => _queue; - ILogger IHaveLogger.Logger => _logger; + if (queueEntry.Value.SendProgressReports) + await ReportProgressAsync(handler, queueEntry).AnyContext(); - public async virtual Task RunAsync(CancellationToken cancellationToken = default) + var lockValue = await handler.GetWorkItemLockAsync(workItemData, cancellationToken).AnyContext(); + if (lockValue == null) { - IQueueEntry queueEntry; + if (handler.Log.IsEnabled(LogLevel.Information)) + handler.Log.LogInformation("Abandoning {TypeName} work item: {Id}: Unable to acquire work item lock.", queueEntry.Value.Type, queueEntry.Id); + + await queueEntry.AbandonAsync().AnyContext(); + return JobResult.Success; + } - using (var timeoutCancellationTokenSource = new CancellationTokenSource(30000)) - using (var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCancellationTokenSource.Token)) + var progressCallback = new Func(async (progress, message) => + { + if (handler.AutoRenewLockOnProgress) { try { - queueEntry = await _queue.DequeueAsync(linkedCancellationTokenSource.Token).AnyContext(); + await Task.WhenAll( + queueEntry.RenewLockAsync(), + lockValue.RenewAsync() + ).AnyContext(); } catch (Exception ex) { - return JobResult.FromException(ex, $"Error trying to dequeue work item: {ex.Message}"); + if (handler.Log.IsEnabled(LogLevel.Error)) + handler.Log.LogError(ex, "Error renewing work item locks: {Message}", ex.Message); } } - return await ProcessAsync(queueEntry, cancellationToken).AnyContext(); - } + await ReportProgressAsync(handler, queueEntry, progress, message).AnyContext(); + if (handler.Log.IsEnabled(LogLevel.Information)) + handler.Log.LogInformation("{TypeName} Progress {Progress}%: {Message}", workItemDataType.Name, progress, message); + }); - public async Task ProcessAsync(IQueueEntry queueEntry, CancellationToken cancellationToken) + try { - if (queueEntry == null) - return JobResult.Success; + handler.LogProcessingQueueEntry(queueEntry, workItemDataType, workItemData); + var workItemContext = new WorkItemContext(workItemData, JobId, lockValue, cancellationToken, progressCallback); + await handler.HandleItemAsync(workItemContext).AnyContext(); - if (cancellationToken.IsCancellationRequested) + if (!workItemContext.Result.IsSuccess) { - await queueEntry.AbandonAsync().AnyContext(); - return JobResult.CancelledWithMessage($"Abandoning {queueEntry.Value.Type} work item: {queueEntry.Id}"); + if (!queueEntry.IsAbandoned && !queueEntry.IsCompleted) + { + await queueEntry.AbandonAsync().AnyContext(); + return workItemContext.Result; + } } - var workItemDataType = GetWorkItemType(queueEntry.Value.Type); - if (workItemDataType == null) + if (!queueEntry.IsAbandoned && !queueEntry.IsCompleted) { - await queueEntry.AbandonAsync().AnyContext(); - return JobResult.FailedWithMessage($"Abandoning {queueEntry.Value.Type} work item: {queueEntry.Id}: Could not resolve work item data type"); + await queueEntry.CompleteAsync().AnyContext(); + handler.LogAutoCompletedQueueEntry(queueEntry, workItemDataType, workItemData); } - using var activity = StartProcessWorkItemActivity(queueEntry, workItemDataType); - using var _ = _logger.BeginScope(s => s - .Property("JobId", JobId) - .Property("QueueEntryId", queueEntry.Id) - .PropertyIf("CorrelationId", queueEntry.CorrelationId, !String.IsNullOrEmpty(queueEntry.CorrelationId)) - .Property("QueueEntryName", workItemDataType.Name)); - - object workItemData; - try - { - workItemData = _queue.Serializer.Deserialize(queueEntry.Value.Data, workItemDataType); - } - catch (Exception ex) - { - await queueEntry.AbandonAsync().AnyContext(); - return JobResult.FromException(ex, $"Abandoning {queueEntry.Value.Type} work item: {queueEntry.Id}: Failed to parse {workItemDataType.Name} work item data"); - } + if (queueEntry.Value.SendProgressReports) + await ReportProgressAsync(handler, queueEntry, 100).AnyContext(); - var handler = _handlers.GetHandler(workItemDataType); - if (handler == null) - { - await queueEntry.CompleteAsync().AnyContext(); - return JobResult.FailedWithMessage($"Completing {queueEntry.Value.Type} work item: {queueEntry.Id}: Handler for type {workItemDataType.Name} not registered"); - } + return JobResult.Success; + } + catch (Exception ex) + { if (queueEntry.Value.SendProgressReports) - await ReportProgressAsync(handler, queueEntry).AnyContext(); + await ReportProgressAsync(handler, queueEntry, -1, $"Failed: {ex.Message}").AnyContext(); - var lockValue = await handler.GetWorkItemLockAsync(workItemData, cancellationToken).AnyContext(); - if (lockValue == null) + if (!queueEntry.IsAbandoned && !queueEntry.IsCompleted) { - if (handler.Log.IsEnabled(LogLevel.Information)) - handler.Log.LogInformation("Abandoning {TypeName} work item: {Id}: Unable to acquire work item lock.", queueEntry.Value.Type, queueEntry.Id); - await queueEntry.AbandonAsync().AnyContext(); - return JobResult.Success; + return JobResult.FromException(ex, $"Abandoning {queueEntry.Value.Type} work item: {queueEntry.Id}: Error in handler {workItemDataType.Name}"); } - var progressCallback = new Func(async (progress, message) => - { - if (handler.AutoRenewLockOnProgress) - { - try - { - await Task.WhenAll( - queueEntry.RenewLockAsync(), - lockValue.RenewAsync() - ).AnyContext(); - } - catch (Exception ex) - { - if (handler.Log.IsEnabled(LogLevel.Error)) - handler.Log.LogError(ex, "Error renewing work item locks: {Message}", ex.Message); - } - } + return JobResult.FromException(ex, $"Error processing {queueEntry.Value.Type} work item: {queueEntry.Id} in handler: {workItemDataType.Name}"); + } + finally + { + await lockValue.ReleaseAsync().AnyContext(); + } + } - await ReportProgressAsync(handler, queueEntry, progress, message).AnyContext(); - if (handler.Log.IsEnabled(LogLevel.Information)) - handler.Log.LogInformation("{TypeName} Progress {Progress}%: {Message}", workItemDataType.Name, progress, message); - }); + protected virtual Activity StartProcessWorkItemActivity(IQueueEntry entry, Type workItemDataType) + { + var activity = FoundatioDiagnostics.ActivitySource.StartActivity("ProcessQueueEntry", ActivityKind.Server, entry.CorrelationId); - try - { - handler.LogProcessingQueueEntry(queueEntry, workItemDataType, workItemData); - var workItemContext = new WorkItemContext(workItemData, JobId, lockValue, cancellationToken, progressCallback); - await handler.HandleItemAsync(workItemContext).AnyContext(); + if (activity == null) + return activity; - if (!workItemContext.Result.IsSuccess) - { - if (!queueEntry.IsAbandoned && !queueEntry.IsCompleted) - { - await queueEntry.AbandonAsync().AnyContext(); - return workItemContext.Result; - } - } + if (entry.Properties != null && entry.Properties.TryGetValue("TraceState", out var traceState)) + activity.TraceStateString = traceState.ToString(); - if (!queueEntry.IsAbandoned && !queueEntry.IsCompleted) - { - await queueEntry.CompleteAsync().AnyContext(); - handler.LogAutoCompletedQueueEntry(queueEntry, workItemDataType, workItemData); - } + activity.DisplayName = $"Work Item: {entry.Value.SubMetricName ?? workItemDataType.Name}"; - if (queueEntry.Value.SendProgressReports) - await ReportProgressAsync(handler, queueEntry, 100).AnyContext(); + EnrichProcessWorkItemActivity(activity, entry, workItemDataType); - return JobResult.Success; - } - catch (Exception ex) - { + return activity; + } - if (queueEntry.Value.SendProgressReports) - await ReportProgressAsync(handler, queueEntry, -1, $"Failed: {ex.Message}").AnyContext(); + protected virtual void EnrichProcessWorkItemActivity(Activity activity, IQueueEntry entry, Type workItemDataType) + { + if (!activity.IsAllDataRequested) + return; - if (!queueEntry.IsAbandoned && !queueEntry.IsCompleted) - { - await queueEntry.AbandonAsync().AnyContext(); - return JobResult.FromException(ex, $"Abandoning {queueEntry.Value.Type} work item: {queueEntry.Id}: Error in handler {workItemDataType.Name}"); - } + activity.AddTag("WorkItemType", entry.Value.Type); + activity.AddTag("Id", entry.Id); + activity.AddTag("CorrelationId", entry.CorrelationId); - return JobResult.FromException(ex, $"Error processing {queueEntry.Value.Type} work item: {queueEntry.Id} in handler: {workItemDataType.Name}"); - } - finally - { - await lockValue.ReleaseAsync().AnyContext(); - } - } + if (entry.Properties == null || entry.Properties.Count <= 0) + return; - protected virtual Activity StartProcessWorkItemActivity(IQueueEntry entry, Type workItemDataType) + foreach (var p in entry.Properties) { - var activity = FoundatioDiagnostics.ActivitySource.StartActivity("ProcessQueueEntry", ActivityKind.Server, entry.CorrelationId); - - if (activity == null) - return activity; - - if (entry.Properties != null && entry.Properties.TryGetValue("TraceState", out var traceState)) - activity.TraceStateString = traceState.ToString(); - - activity.DisplayName = $"Work Item: {entry.Value.SubMetricName ?? workItemDataType.Name}"; - - EnrichProcessWorkItemActivity(activity, entry, workItemDataType); - - return activity; + if (p.Key != "TraceState") + activity.AddTag(p.Key, p.Value); } + } - protected virtual void EnrichProcessWorkItemActivity(Activity activity, IQueueEntry entry, Type workItemDataType) + private readonly ConcurrentDictionary _knownTypesCache = new(); + protected virtual Type GetWorkItemType(string workItemType) + { + return _knownTypesCache.GetOrAdd(workItemType, type => { - if (!activity.IsAllDataRequested) - return; - - activity.AddTag("WorkItemType", entry.Value.Type); - activity.AddTag("Id", entry.Id); - activity.AddTag("CorrelationId", entry.CorrelationId); - - if (entry.Properties == null || entry.Properties.Count <= 0) - return; - - foreach (var p in entry.Properties) + try { - if (p.Key != "TraceState") - activity.AddTag(p.Key, p.Value); + return Type.GetType(type); } - } - - private readonly ConcurrentDictionary _knownTypesCache = new(); - protected virtual Type GetWorkItemType(string workItemType) - { - return _knownTypesCache.GetOrAdd(workItemType, type => + catch (Exception) { try { + string[] typeParts = type.Split(','); + if (typeParts.Length >= 2) + type = String.Join(",", typeParts[0], typeParts[1]); + + // try resolve type without version return Type.GetType(type); } - catch (Exception) + catch (Exception ex) { - try - { - string[] typeParts = type.Split(','); - if (typeParts.Length >= 2) - type = String.Join(",", typeParts[0], typeParts[1]); - - // try resolve type without version - return Type.GetType(type); - } - catch (Exception ex) - { - if (_logger.IsEnabled(LogLevel.Warning)) - _logger.LogWarning(ex, "Error getting work item type: {WorkItemType}", type); - - return null; - } + if (_logger.IsEnabled(LogLevel.Warning)) + _logger.LogWarning(ex, "Error getting work item type: {WorkItemType}", type); + + return null; } - }); - } + } + }); + } - protected async Task ReportProgressAsync(IWorkItemHandler handler, IQueueEntry queueEntry, int progress = 0, string message = null) + protected async Task ReportProgressAsync(IWorkItemHandler handler, IQueueEntry queueEntry, int progress = 0, string message = null) + { + try { - try + await _publisher.PublishAsync(new WorkItemStatus { - await _publisher.PublishAsync(new WorkItemStatus - { - WorkItemId = queueEntry.Value.WorkItemId, - Type = queueEntry.Value.Type, - Progress = progress, - Message = message - }).AnyContext(); - } - catch (Exception ex) - { - if (handler.Log.IsEnabled(LogLevel.Error)) - handler.Log.LogError(ex, "Error sending progress report: {Message}", ex.Message); - } + WorkItemId = queueEntry.Value.WorkItemId, + Type = queueEntry.Value.Type, + Progress = progress, + Message = message + }).AnyContext(); + } + catch (Exception ex) + { + if (handler.Log.IsEnabled(LogLevel.Error)) + handler.Log.LogError(ex, "Error sending progress report: {Message}", ex.Message); } } } diff --git a/src/Foundatio/Jobs/WorkItemJob/WorkItemQueueExtensions.cs b/src/Foundatio/Jobs/WorkItemJob/WorkItemQueueExtensions.cs index 18f1a5a9d..363087381 100644 --- a/src/Foundatio/Jobs/WorkItemJob/WorkItemQueueExtensions.cs +++ b/src/Foundatio/Jobs/WorkItemJob/WorkItemQueueExtensions.cs @@ -5,57 +5,56 @@ using Foundatio.Serializer; using Foundatio.Utility; -namespace Foundatio.Jobs +namespace Foundatio.Jobs; + +public static class WorkItemQueueExtensions { - public static class WorkItemQueueExtensions + public static async Task EnqueueAsync(this IQueue queue, T workItemData, bool includeProgressReporting = false) { - public static async Task EnqueueAsync(this IQueue queue, T workItemData, bool includeProgressReporting = false) - { - string jobId = Guid.NewGuid().ToString("N"); - var bytes = queue.Serializer.SerializeToBytes(workItemData); + string jobId = Guid.NewGuid().ToString("N"); + var bytes = queue.Serializer.SerializeToBytes(workItemData); - var data = new WorkItemData - { - Data = bytes, - WorkItemId = jobId, - Type = typeof(T).AssemblyQualifiedName, - SendProgressReports = includeProgressReporting - }; + var data = new WorkItemData + { + Data = bytes, + WorkItemId = jobId, + Type = typeof(T).AssemblyQualifiedName, + SendProgressReports = includeProgressReporting + }; - if (workItemData is IHaveUniqueIdentifier haveUniqueIdentifier) - data.UniqueIdentifier = haveUniqueIdentifier.UniqueIdentifier; + if (workItemData is IHaveUniqueIdentifier haveUniqueIdentifier) + data.UniqueIdentifier = haveUniqueIdentifier.UniqueIdentifier; - if (workItemData is IHaveSubMetricName haveSubMetricName && haveSubMetricName.SubMetricName != null) - data.SubMetricName = haveSubMetricName.SubMetricName; - else - data.SubMetricName = GetDefaultSubMetricName(data); + if (workItemData is IHaveSubMetricName haveSubMetricName && haveSubMetricName.SubMetricName != null) + data.SubMetricName = haveSubMetricName.SubMetricName; + else + data.SubMetricName = GetDefaultSubMetricName(data); - await queue.EnqueueAsync(data).AnyContext(); + await queue.EnqueueAsync(data).AnyContext(); - return jobId; - } + return jobId; + } - private static string GetDefaultSubMetricName(WorkItemData data) - { - if (String.IsNullOrEmpty(data.Type)) - return null; + private static string GetDefaultSubMetricName(WorkItemData data) + { + if (String.IsNullOrEmpty(data.Type)) + return null; - string type = GetTypeName(data.Type); - if (type != null && type.EndsWith("WorkItem")) - type = type.Substring(0, type.Length - 8); + string type = GetTypeName(data.Type); + if (type != null && type.EndsWith("WorkItem")) + type = type.Substring(0, type.Length - 8); - return type?.ToLowerInvariant(); - } + return type?.ToLowerInvariant(); + } - private static string GetTypeName(string assemblyQualifiedName) - { - if (String.IsNullOrEmpty(assemblyQualifiedName)) - return null; + private static string GetTypeName(string assemblyQualifiedName) + { + if (String.IsNullOrEmpty(assemblyQualifiedName)) + return null; - var parts = assemblyQualifiedName.Split(','); - int i = parts[0].LastIndexOf('.'); + var parts = assemblyQualifiedName.Split(','); + int i = parts[0].LastIndexOf('.'); - return i < 0 ? null : parts[0].Substring(i + 1); - } + return i < 0 ? null : parts[0].Substring(i + 1); } } diff --git a/src/Foundatio/Jobs/WorkItemJob/WorkItemStatus.cs b/src/Foundatio/Jobs/WorkItemJob/WorkItemStatus.cs index 6b460f255..18eef4595 100644 --- a/src/Foundatio/Jobs/WorkItemJob/WorkItemStatus.cs +++ b/src/Foundatio/Jobs/WorkItemJob/WorkItemStatus.cs @@ -1,10 +1,9 @@ -namespace Foundatio.Jobs +namespace Foundatio.Jobs; + +public class WorkItemStatus { - public class WorkItemStatus - { - public string WorkItemId { get; set; } - public int Progress { get; set; } - public string Message { get; set; } - public string Type { get; set; } - } + public string WorkItemId { get; set; } + public int Progress { get; set; } + public string Message { get; set; } + public string Type { get; set; } } diff --git a/src/Foundatio/Lock/CacheLockProvider.cs b/src/Foundatio/Lock/CacheLockProvider.cs index 090ede7b3..f1841755d 100644 --- a/src/Foundatio/Lock/CacheLockProvider.cs +++ b/src/Foundatio/Lock/CacheLockProvider.cs @@ -11,238 +11,237 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -namespace Foundatio.Lock +namespace Foundatio.Lock; + +public class CacheLockProvider : ILockProvider, IHaveLogger { - public class CacheLockProvider : ILockProvider, IHaveLogger + private readonly ICacheClient _cacheClient; + private readonly IMessageBus _messageBus; + private readonly ConcurrentDictionary _autoResetEvents = new(); + private readonly AsyncLock _lock = new(); + private bool _isSubscribed; + private readonly ILogger _logger; + private readonly Histogram _lockWaitTimeHistogram; + private readonly Counter _lockTimeoutCounter; + + public CacheLockProvider(ICacheClient cacheClient, IMessageBus messageBus, ILoggerFactory loggerFactory = null) { - private readonly ICacheClient _cacheClient; - private readonly IMessageBus _messageBus; - private readonly ConcurrentDictionary _autoResetEvents = new(); - private readonly AsyncLock _lock = new(); - private bool _isSubscribed; - private readonly ILogger _logger; - private readonly Histogram _lockWaitTimeHistogram; - private readonly Counter _lockTimeoutCounter; - - public CacheLockProvider(ICacheClient cacheClient, IMessageBus messageBus, ILoggerFactory loggerFactory = null) - { - _logger = loggerFactory?.CreateLogger() ?? NullLogger.Instance; - _cacheClient = new ScopedCacheClient(cacheClient, "lock"); - _messageBus = messageBus; + _logger = loggerFactory?.CreateLogger() ?? NullLogger.Instance; + _cacheClient = new ScopedCacheClient(cacheClient, "lock"); + _messageBus = messageBus; - _lockWaitTimeHistogram = FoundatioDiagnostics.Meter.CreateHistogram("foundatio.lock.wait.time", description: "Time waiting for locks", unit: "ms"); - _lockTimeoutCounter = FoundatioDiagnostics.Meter.CreateCounter("foundatio.lock.failed", description: "Number of failed attempts to acquire a lock"); - } + _lockWaitTimeHistogram = FoundatioDiagnostics.Meter.CreateHistogram("foundatio.lock.wait.time", description: "Time waiting for locks", unit: "ms"); + _lockTimeoutCounter = FoundatioDiagnostics.Meter.CreateCounter("foundatio.lock.failed", description: "Number of failed attempts to acquire a lock"); + } - ILogger IHaveLogger.Logger => _logger; + ILogger IHaveLogger.Logger => _logger; - private async Task EnsureTopicSubscriptionAsync() + private async Task EnsureTopicSubscriptionAsync() + { + if (_isSubscribed) + return; + + using (await _lock.LockAsync().AnyContext()) { if (_isSubscribed) return; - using (await _lock.LockAsync().AnyContext()) - { - if (_isSubscribed) - return; - - bool isTraceLogLevelEnabled = _logger.IsEnabled(LogLevel.Trace); - if (isTraceLogLevelEnabled) _logger.LogTrace("Subscribing to cache lock released"); - await _messageBus.SubscribeAsync(OnLockReleasedAsync).AnyContext(); - _isSubscribed = true; - if (isTraceLogLevelEnabled) _logger.LogTrace("Subscribed to cache lock released"); - } + bool isTraceLogLevelEnabled = _logger.IsEnabled(LogLevel.Trace); + if (isTraceLogLevelEnabled) _logger.LogTrace("Subscribing to cache lock released"); + await _messageBus.SubscribeAsync(OnLockReleasedAsync).AnyContext(); + _isSubscribed = true; + if (isTraceLogLevelEnabled) _logger.LogTrace("Subscribed to cache lock released"); } + } - private Task OnLockReleasedAsync(CacheLockReleased msg, CancellationToken cancellationToken = default) - { - if (_logger.IsEnabled(LogLevel.Trace)) - _logger.LogTrace("Got lock released message: {Resource} ({LockId})", msg.Resource, msg.LockId); + private Task OnLockReleasedAsync(CacheLockReleased msg, CancellationToken cancellationToken = default) + { + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.LogTrace("Got lock released message: {Resource} ({LockId})", msg.Resource, msg.LockId); - if (_autoResetEvents.TryGetValue(msg.Resource, out var autoResetEvent)) - autoResetEvent.Target.Set(); + if (_autoResetEvents.TryGetValue(msg.Resource, out var autoResetEvent)) + autoResetEvent.Target.Set(); - return Task.CompletedTask; - } + return Task.CompletedTask; + } - protected virtual Activity StartLockActivity(string resource) - { - var activity = FoundatioDiagnostics.ActivitySource.StartActivity("AcquireLock"); + protected virtual Activity StartLockActivity(string resource) + { + var activity = FoundatioDiagnostics.ActivitySource.StartActivity("AcquireLock"); - if (activity == null) - return activity; + if (activity == null) + return activity; - activity.AddTag("resource", resource); - activity.DisplayName = $"Lock: {resource}"; + activity.AddTag("resource", resource); + activity.DisplayName = $"Lock: {resource}"; - return activity; - } + return activity; + } - public async Task AcquireAsync(string resource, TimeSpan? timeUntilExpires = null, bool releaseOnDispose = true, CancellationToken cancellationToken = default) - { - bool isTraceLogLevelEnabled = _logger.IsEnabled(LogLevel.Trace); - bool isDebugLogLevelEnabled = _logger.IsEnabled(LogLevel.Debug); - bool shouldWait = !cancellationToken.IsCancellationRequested; - if (isDebugLogLevelEnabled) - _logger.LogDebug("Attempting to acquire lock: {Resource}", resource); + public async Task AcquireAsync(string resource, TimeSpan? timeUntilExpires = null, bool releaseOnDispose = true, CancellationToken cancellationToken = default) + { + bool isTraceLogLevelEnabled = _logger.IsEnabled(LogLevel.Trace); + bool isDebugLogLevelEnabled = _logger.IsEnabled(LogLevel.Debug); + bool shouldWait = !cancellationToken.IsCancellationRequested; + if (isDebugLogLevelEnabled) + _logger.LogDebug("Attempting to acquire lock: {Resource}", resource); - if (!timeUntilExpires.HasValue) - timeUntilExpires = TimeSpan.FromMinutes(20); + if (!timeUntilExpires.HasValue) + timeUntilExpires = TimeSpan.FromMinutes(20); - using var activity = StartLockActivity(resource); + using var activity = StartLockActivity(resource); - bool gotLock = false; - string lockId = GenerateNewLockId(); - var sw = Stopwatch.StartNew(); - try + bool gotLock = false; + string lockId = GenerateNewLockId(); + var sw = Stopwatch.StartNew(); + try + { + do { - do + try { - try - { - if (timeUntilExpires.Value == TimeSpan.Zero) // no lock timeout - gotLock = await _cacheClient.AddAsync(resource, lockId).AnyContext(); - else - gotLock = await _cacheClient.AddAsync(resource, lockId, timeUntilExpires).AnyContext(); - } - catch { } + if (timeUntilExpires.Value == TimeSpan.Zero) // no lock timeout + gotLock = await _cacheClient.AddAsync(resource, lockId).AnyContext(); + else + gotLock = await _cacheClient.AddAsync(resource, lockId, timeUntilExpires).AnyContext(); + } + catch { } - if (gotLock) - break; + if (gotLock) + break; - if (isDebugLogLevelEnabled) - _logger.LogDebug("Failed to acquire lock: {Resource}", resource); + if (isDebugLogLevelEnabled) + _logger.LogDebug("Failed to acquire lock: {Resource}", resource); - if (cancellationToken.IsCancellationRequested) - { - if (isTraceLogLevelEnabled && shouldWait) - _logger.LogTrace("Cancellation requested"); + if (cancellationToken.IsCancellationRequested) + { + if (isTraceLogLevelEnabled && shouldWait) + _logger.LogTrace("Cancellation requested"); - break; - } + break; + } - var autoResetEvent = _autoResetEvents.AddOrUpdate(resource, new ResetEventWithRefCount { RefCount = 1, Target = new AsyncAutoResetEvent() }, (n, e) => { e.RefCount++; return e; }); - if (!_isSubscribed) - await EnsureTopicSubscriptionAsync().AnyContext(); + var autoResetEvent = _autoResetEvents.AddOrUpdate(resource, new ResetEventWithRefCount { RefCount = 1, Target = new AsyncAutoResetEvent() }, (n, e) => { e.RefCount++; return e; }); + if (!_isSubscribed) + await EnsureTopicSubscriptionAsync().AnyContext(); - var keyExpiration = SystemClock.UtcNow.SafeAdd(await _cacheClient.GetExpirationAsync(resource).AnyContext() ?? TimeSpan.Zero); - var delayAmount = keyExpiration.Subtract(SystemClock.UtcNow); + var keyExpiration = SystemClock.UtcNow.SafeAdd(await _cacheClient.GetExpirationAsync(resource).AnyContext() ?? TimeSpan.Zero); + var delayAmount = keyExpiration.Subtract(SystemClock.UtcNow); - // delay a minimum of 50ms - if (delayAmount < TimeSpan.FromMilliseconds(50)) - delayAmount = TimeSpan.FromMilliseconds(50); + // delay a minimum of 50ms + if (delayAmount < TimeSpan.FromMilliseconds(50)) + delayAmount = TimeSpan.FromMilliseconds(50); - // delay a maximum of 3 seconds - if (delayAmount > TimeSpan.FromSeconds(3)) - delayAmount = TimeSpan.FromSeconds(3); + // delay a maximum of 3 seconds + if (delayAmount > TimeSpan.FromSeconds(3)) + delayAmount = TimeSpan.FromSeconds(3); - if (isTraceLogLevelEnabled) - _logger.LogTrace("Will wait {Delay:g} before retrying to acquire lock: {Resource}", delayAmount, resource); + if (isTraceLogLevelEnabled) + _logger.LogTrace("Will wait {Delay:g} before retrying to acquire lock: {Resource}", delayAmount, resource); - // wait until we get a message saying the lock was released or 3 seconds has elapsed or cancellation has been requested - using (var maxWaitCancellationTokenSource = new CancellationTokenSource(delayAmount)) - using (var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, maxWaitCancellationTokenSource.Token)) + // wait until we get a message saying the lock was released or 3 seconds has elapsed or cancellation has been requested + using (var maxWaitCancellationTokenSource = new CancellationTokenSource(delayAmount)) + using (var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, maxWaitCancellationTokenSource.Token)) + { + try { - try - { - await autoResetEvent.Target.WaitAsync(linkedCancellationTokenSource.Token).AnyContext(); - } - catch (OperationCanceledException) { } + await autoResetEvent.Target.WaitAsync(linkedCancellationTokenSource.Token).AnyContext(); } + catch (OperationCanceledException) { } + } - Thread.Yield(); - } while (!cancellationToken.IsCancellationRequested); - } - finally + Thread.Yield(); + } while (!cancellationToken.IsCancellationRequested); + } + finally + { + bool shouldRemove = false; + _autoResetEvents.TryUpdate(resource, (n, e) => { - bool shouldRemove = false; - _autoResetEvents.TryUpdate(resource, (n, e) => - { - e.RefCount--; - if (e.RefCount == 0) - shouldRemove = true; - return e; - }); - - if (shouldRemove) - _autoResetEvents.TryRemove(resource, out var _); - } - sw.Stop(); + e.RefCount--; + if (e.RefCount == 0) + shouldRemove = true; + return e; + }); + + if (shouldRemove) + _autoResetEvents.TryRemove(resource, out var _); + } + sw.Stop(); - _lockWaitTimeHistogram.Record(sw.Elapsed.TotalMilliseconds); + _lockWaitTimeHistogram.Record(sw.Elapsed.TotalMilliseconds); - if (!gotLock) - { - _lockTimeoutCounter.Add(1); + if (!gotLock) + { + _lockTimeoutCounter.Add(1); - if (cancellationToken.IsCancellationRequested && isTraceLogLevelEnabled) - _logger.LogTrace("Cancellation requested for lock {Resource} after {Duration:g}", resource, sw.Elapsed); - else if (_logger.IsEnabled(LogLevel.Warning)) - _logger.LogWarning("Failed to acquire lock {Resource} after {Duration:g}", resource, lockId, sw.Elapsed); + if (cancellationToken.IsCancellationRequested && isTraceLogLevelEnabled) + _logger.LogTrace("Cancellation requested for lock {Resource} after {Duration:g}", resource, sw.Elapsed); + else if (_logger.IsEnabled(LogLevel.Warning)) + _logger.LogWarning("Failed to acquire lock {Resource} after {Duration:g}", resource, lockId, sw.Elapsed); - return null; - } + return null; + } - if (sw.Elapsed > TimeSpan.FromSeconds(5) && _logger.IsEnabled(LogLevel.Warning)) - _logger.LogWarning("Acquired lock {Resource} ({LockId}) after {Duration:g}", resource, lockId, sw.Elapsed); - else if (_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("Acquired lock {Resource} ({LockId}) after {Duration:g}", resource, lockId, sw.Elapsed); + if (sw.Elapsed > TimeSpan.FromSeconds(5) && _logger.IsEnabled(LogLevel.Warning)) + _logger.LogWarning("Acquired lock {Resource} ({LockId}) after {Duration:g}", resource, lockId, sw.Elapsed); + else if (_logger.IsEnabled(LogLevel.Debug)) + _logger.LogDebug("Acquired lock {Resource} ({LockId}) after {Duration:g}", resource, lockId, sw.Elapsed); - return new DisposableLock(resource, lockId, sw.Elapsed, this, _logger, releaseOnDispose); - } + return new DisposableLock(resource, lockId, sw.Elapsed, this, _logger, releaseOnDispose); + } - public async Task IsLockedAsync(string resource) - { - var result = await Run.WithRetriesAsync(() => _cacheClient.ExistsAsync(resource), logger: _logger).AnyContext(); - return result; - } + public async Task IsLockedAsync(string resource) + { + var result = await Run.WithRetriesAsync(() => _cacheClient.ExistsAsync(resource), logger: _logger).AnyContext(); + return result; + } - public async Task ReleaseAsync(string resource, string lockId) - { - if (_logger.IsEnabled(LogLevel.Trace)) - _logger.LogTrace("ReleaseAsync Start: {Resource} ({LockId})", resource, lockId); + public async Task ReleaseAsync(string resource, string lockId) + { + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.LogTrace("ReleaseAsync Start: {Resource} ({LockId})", resource, lockId); - await Run.WithRetriesAsync(() => _cacheClient.RemoveIfEqualAsync(resource, lockId), 15, logger: _logger).AnyContext(); - await _messageBus.PublishAsync(new CacheLockReleased { Resource = resource, LockId = lockId }).AnyContext(); + await Run.WithRetriesAsync(() => _cacheClient.RemoveIfEqualAsync(resource, lockId), 15, logger: _logger).AnyContext(); + await _messageBus.PublishAsync(new CacheLockReleased { Resource = resource, LockId = lockId }).AnyContext(); - if (_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("Released lock: {Resource} ({LockId})", resource, lockId); - } + if (_logger.IsEnabled(LogLevel.Debug)) + _logger.LogDebug("Released lock: {Resource} ({LockId})", resource, lockId); + } - public Task RenewAsync(string resource, string lockId, TimeSpan? timeUntilExpires = null) - { - if (!timeUntilExpires.HasValue) - timeUntilExpires = TimeSpan.FromMinutes(20); + public Task RenewAsync(string resource, string lockId, TimeSpan? timeUntilExpires = null) + { + if (!timeUntilExpires.HasValue) + timeUntilExpires = TimeSpan.FromMinutes(20); - if (_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("Renewing lock {Resource} ({LockId}) for {Duration:g}", resource, lockId, timeUntilExpires); + if (_logger.IsEnabled(LogLevel.Debug)) + _logger.LogDebug("Renewing lock {Resource} ({LockId}) for {Duration:g}", resource, lockId, timeUntilExpires); - return Run.WithRetriesAsync(() => _cacheClient.ReplaceIfEqualAsync(resource, lockId, lockId, timeUntilExpires.Value)); - } + return Run.WithRetriesAsync(() => _cacheClient.ReplaceIfEqualAsync(resource, lockId, lockId, timeUntilExpires.Value)); + } - private class ResetEventWithRefCount - { - public int RefCount { get; set; } - public AsyncAutoResetEvent Target { get; set; } - } + private class ResetEventWithRefCount + { + public int RefCount { get; set; } + public AsyncAutoResetEvent Target { get; set; } + } - private static string _allowedChars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; - private static Random _rng = new(); + private static string _allowedChars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + private static Random _rng = new(); - private string GenerateNewLockId() - { - char[] chars = new char[16]; + private string GenerateNewLockId() + { + char[] chars = new char[16]; - for (int i = 0; i < 16; ++i) - chars[i] = _allowedChars[_rng.Next(62)]; + for (int i = 0; i < 16; ++i) + chars[i] = _allowedChars[_rng.Next(62)]; - return new string(chars, 0, 16); - } + return new string(chars, 0, 16); } +} - public class CacheLockReleased - { - public string Resource { get; set; } - public string LockId { get; set; } - } +public class CacheLockReleased +{ + public string Resource { get; set; } + public string LockId { get; set; } } diff --git a/src/Foundatio/Lock/DisposableLock.cs b/src/Foundatio/Lock/DisposableLock.cs index 9afc4a0fa..fa243bbfe 100644 --- a/src/Foundatio/Lock/DisposableLock.cs +++ b/src/Foundatio/Lock/DisposableLock.cs @@ -4,89 +4,88 @@ using Foundatio.Utility; using Microsoft.Extensions.Logging; -namespace Foundatio.Lock +namespace Foundatio.Lock; + +internal class DisposableLock : ILock { - internal class DisposableLock : ILock + private readonly ILockProvider _lockProvider; + private readonly ILogger _logger; + private bool _isReleased; + private int _renewalCount; + private readonly object _lock = new(); + private readonly Stopwatch _duration; + private readonly bool _shouldReleaseOnDispose; + + public DisposableLock(string resource, string lockId, TimeSpan timeWaitedForLock, ILockProvider lockProvider, ILogger logger, bool shouldReleaseOnDispose) { - private readonly ILockProvider _lockProvider; - private readonly ILogger _logger; - private bool _isReleased; - private int _renewalCount; - private readonly object _lock = new(); - private readonly Stopwatch _duration; - private readonly bool _shouldReleaseOnDispose; - - public DisposableLock(string resource, string lockId, TimeSpan timeWaitedForLock, ILockProvider lockProvider, ILogger logger, bool shouldReleaseOnDispose) - { - Resource = resource; - LockId = lockId; - TimeWaitedForLock = timeWaitedForLock; - AcquiredTimeUtc = SystemClock.UtcNow; - _duration = Stopwatch.StartNew(); - _logger = logger; - _lockProvider = lockProvider; - _shouldReleaseOnDispose = shouldReleaseOnDispose; - } + Resource = resource; + LockId = lockId; + TimeWaitedForLock = timeWaitedForLock; + AcquiredTimeUtc = SystemClock.UtcNow; + _duration = Stopwatch.StartNew(); + _logger = logger; + _lockProvider = lockProvider; + _shouldReleaseOnDispose = shouldReleaseOnDispose; + } - public string LockId { get; } - public string Resource { get; } - public DateTime AcquiredTimeUtc { get; } - public TimeSpan TimeWaitedForLock { get; } - public int RenewalCount => _renewalCount; + public string LockId { get; } + public string Resource { get; } + public DateTime AcquiredTimeUtc { get; } + public TimeSpan TimeWaitedForLock { get; } + public int RenewalCount => _renewalCount; - public async ValueTask DisposeAsync() + public async ValueTask DisposeAsync() + { + if (!_shouldReleaseOnDispose) + return; + + bool isTraceLogLevelEnabled = _logger.IsEnabled(LogLevel.Trace); + if (isTraceLogLevelEnabled) + _logger.LogTrace("Disposing lock {Resource}", Resource); + + try { - if (!_shouldReleaseOnDispose) - return; - - bool isTraceLogLevelEnabled = _logger.IsEnabled(LogLevel.Trace); - if (isTraceLogLevelEnabled) - _logger.LogTrace("Disposing lock {Resource}", Resource); - - try - { - await ReleaseAsync().AnyContext(); - } - catch (Exception ex) - { - if (_logger.IsEnabled(LogLevel.Error)) - _logger.LogError(ex, "Unable to release lock {Resource}", Resource); - } - - if (isTraceLogLevelEnabled) - _logger.LogTrace("Disposed lock {Resource}", Resource); + await ReleaseAsync().AnyContext(); } - - public async Task RenewAsync(TimeSpan? lockExtension = null) + catch (Exception ex) { - if (_logger.IsEnabled(LogLevel.Trace)) - _logger.LogTrace("Renewing lock {Resource}", Resource); + if (_logger.IsEnabled(LogLevel.Error)) + _logger.LogError(ex, "Unable to release lock {Resource}", Resource); + } - await _lockProvider.RenewAsync(Resource, LockId, lockExtension).AnyContext(); - _renewalCount++; + if (isTraceLogLevelEnabled) + _logger.LogTrace("Disposed lock {Resource}", Resource); + } - if (_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("Renewed lock {Resource}", Resource); - } + public async Task RenewAsync(TimeSpan? lockExtension = null) + { + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.LogTrace("Renewing lock {Resource}", Resource); + + await _lockProvider.RenewAsync(Resource, LockId, lockExtension).AnyContext(); + _renewalCount++; + + if (_logger.IsEnabled(LogLevel.Debug)) + _logger.LogDebug("Renewed lock {Resource}", Resource); + } + + public Task ReleaseAsync() + { + if (_isReleased) + return Task.CompletedTask; - public Task ReleaseAsync() + lock (_lock) { if (_isReleased) return Task.CompletedTask; - lock (_lock) - { - if (_isReleased) - return Task.CompletedTask; + _isReleased = true; + _duration.Stop(); - _isReleased = true; - _duration.Stop(); - - if (_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("Releasing lock {Resource} after {Duration:g}", Resource, _duration.Elapsed); + if (_logger.IsEnabled(LogLevel.Debug)) + _logger.LogDebug("Releasing lock {Resource} after {Duration:g}", Resource, _duration.Elapsed); - return _lockProvider.ReleaseAsync(Resource, LockId); - } + return _lockProvider.ReleaseAsync(Resource, LockId); } } } diff --git a/src/Foundatio/Lock/DisposableLockCollection.cs b/src/Foundatio/Lock/DisposableLockCollection.cs index a8d85f190..1f61ba988 100644 --- a/src/Foundatio/Lock/DisposableLockCollection.cs +++ b/src/Foundatio/Lock/DisposableLockCollection.cs @@ -6,88 +6,87 @@ using Foundatio.Utility; using Microsoft.Extensions.Logging; -namespace Foundatio.Lock +namespace Foundatio.Lock; + +internal class DisposableLockCollection : ILock { - internal class DisposableLockCollection : ILock + private readonly List _locks = new(); + private readonly ILogger _logger; + private bool _isReleased; + private int _renewalCount; + private readonly object _lock = new(); + private readonly Stopwatch _duration; + + public DisposableLockCollection(IEnumerable locks, string lockId, TimeSpan timeWaitedForLock, ILogger logger) { - private readonly List _locks = new(); - private readonly ILogger _logger; - private bool _isReleased; - private int _renewalCount; - private readonly object _lock = new(); - private readonly Stopwatch _duration; - - public DisposableLockCollection(IEnumerable locks, string lockId, TimeSpan timeWaitedForLock, ILogger logger) - { - if (locks == null) - throw new ArgumentNullException(nameof(locks)); - - _locks.AddRange(locks); - Resource = String.Join("+", _locks.Select(l => l.Resource)); - LockId = lockId; - TimeWaitedForLock = timeWaitedForLock; - AcquiredTimeUtc = SystemClock.UtcNow; - _duration = Stopwatch.StartNew(); - _logger = logger; - } + if (locks == null) + throw new ArgumentNullException(nameof(locks)); - public IReadOnlyCollection Locks => _locks.AsReadOnly(); - public string LockId { get; } - public string Resource { get; } - public DateTime AcquiredTimeUtc { get; } - public TimeSpan TimeWaitedForLock { get; } - public int RenewalCount => _renewalCount; + _locks.AddRange(locks); + Resource = String.Join("+", _locks.Select(l => l.Resource)); + LockId = lockId; + TimeWaitedForLock = timeWaitedForLock; + AcquiredTimeUtc = SystemClock.UtcNow; + _duration = Stopwatch.StartNew(); + _logger = logger; + } - public async Task RenewAsync(TimeSpan? lockExtension = null) - { - if (_logger.IsEnabled(LogLevel.Trace)) - _logger.LogTrace("Renewing {LockCount} locks {Resource}", _locks.Count, Resource); + public IReadOnlyCollection Locks => _locks.AsReadOnly(); + public string LockId { get; } + public string Resource { get; } + public DateTime AcquiredTimeUtc { get; } + public TimeSpan TimeWaitedForLock { get; } + public int RenewalCount => _renewalCount; - await Task.WhenAll(_locks.Select(l => l.RenewAsync(lockExtension))).AnyContext(); - _renewalCount++; + public async Task RenewAsync(TimeSpan? lockExtension = null) + { + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.LogTrace("Renewing {LockCount} locks {Resource}", _locks.Count, Resource); - if (_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("Renewing {LockCount} locks {Resource}", _locks.Count, Resource); - } + await Task.WhenAll(_locks.Select(l => l.RenewAsync(lockExtension))).AnyContext(); + _renewalCount++; + + if (_logger.IsEnabled(LogLevel.Debug)) + _logger.LogDebug("Renewing {LockCount} locks {Resource}", _locks.Count, Resource); + } - public Task ReleaseAsync() + public Task ReleaseAsync() + { + if (_isReleased) + return Task.CompletedTask; + + lock (_lock) { if (_isReleased) return Task.CompletedTask; - lock (_lock) - { - if (_isReleased) - return Task.CompletedTask; - - _isReleased = true; - _duration.Stop(); + _isReleased = true; + _duration.Stop(); - if (_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("Releasing {LockCount} locks {Resource} after {Duration:g}", _locks.Count, Resource, _duration.Elapsed); + if (_logger.IsEnabled(LogLevel.Debug)) + _logger.LogDebug("Releasing {LockCount} locks {Resource} after {Duration:g}", _locks.Count, Resource, _duration.Elapsed); - return Task.WhenAll(_locks.Select(l => l.ReleaseAsync())); - } + return Task.WhenAll(_locks.Select(l => l.ReleaseAsync())); } + } - public async ValueTask DisposeAsync() + public async ValueTask DisposeAsync() + { + bool isTraceLogLevelEnabled = _logger.IsEnabled(LogLevel.Trace); + if (isTraceLogLevelEnabled) + _logger.LogTrace("Disposing {LockCount} locks {Resource}", _locks.Count, Resource); + + try { - bool isTraceLogLevelEnabled = _logger.IsEnabled(LogLevel.Trace); - if (isTraceLogLevelEnabled) - _logger.LogTrace("Disposing {LockCount} locks {Resource}", _locks.Count, Resource); - - try - { - await Task.WhenAll(_locks.Select(l => l.ReleaseAsync())).AnyContext(); - } - catch (Exception ex) - { - if (_logger.IsEnabled(LogLevel.Error)) - _logger.LogError(ex, "Unable to release {LockCount} locks {Resource}", _locks.Count, Resource); - } - - if (isTraceLogLevelEnabled) - _logger.LogTrace("Disposed {LockCount} locks {Resource}", _locks.Count, Resource); + await Task.WhenAll(_locks.Select(l => l.ReleaseAsync())).AnyContext(); } + catch (Exception ex) + { + if (_logger.IsEnabled(LogLevel.Error)) + _logger.LogError(ex, "Unable to release {LockCount} locks {Resource}", _locks.Count, Resource); + } + + if (isTraceLogLevelEnabled) + _logger.LogTrace("Disposed {LockCount} locks {Resource}", _locks.Count, Resource); } } diff --git a/src/Foundatio/Lock/ILockProvider.cs b/src/Foundatio/Lock/ILockProvider.cs index b7569e9ab..b70346a4b 100644 --- a/src/Foundatio/Lock/ILockProvider.cs +++ b/src/Foundatio/Lock/ILockProvider.cs @@ -8,274 +8,273 @@ using Foundatio.Utility; using Microsoft.Extensions.Logging; -namespace Foundatio.Lock +namespace Foundatio.Lock; + +public interface ILockProvider +{ + Task AcquireAsync(string resource, TimeSpan? timeUntilExpires = null, bool releaseOnDispose = true, CancellationToken cancellationToken = default); + Task IsLockedAsync(string resource); + Task ReleaseAsync(string resource, string lockId); + Task RenewAsync(string resource, string lockId, TimeSpan? timeUntilExpires = null); +} + +public interface ILock : IAsyncDisposable +{ + Task RenewAsync(TimeSpan? timeUntilExpires = null); + Task ReleaseAsync(); + string LockId { get; } + string Resource { get; } + DateTime AcquiredTimeUtc { get; } + TimeSpan TimeWaitedForLock { get; } + int RenewalCount { get; } +} + +public static class LockProviderExtensions { - public interface ILockProvider + public static Task ReleaseAsync(this ILockProvider provider, ILock @lock) { - Task AcquireAsync(string resource, TimeSpan? timeUntilExpires = null, bool releaseOnDispose = true, CancellationToken cancellationToken = default); - Task IsLockedAsync(string resource); - Task ReleaseAsync(string resource, string lockId); - Task RenewAsync(string resource, string lockId, TimeSpan? timeUntilExpires = null); + return provider.ReleaseAsync(@lock.Resource, @lock.LockId); } - public interface ILock : IAsyncDisposable + public static Task RenewAsync(this ILockProvider provider, ILock @lock, TimeSpan? timeUntilExpires = null) { - Task RenewAsync(TimeSpan? timeUntilExpires = null); - Task ReleaseAsync(); - string LockId { get; } - string Resource { get; } - DateTime AcquiredTimeUtc { get; } - TimeSpan TimeWaitedForLock { get; } - int RenewalCount { get; } + return provider.RenewAsync(@lock.Resource, @lock.LockId, timeUntilExpires); } - public static class LockProviderExtensions + public static Task AcquireAsync(this ILockProvider provider, string resource, TimeSpan? timeUntilExpires = null, CancellationToken cancellationToken = default) { - public static Task ReleaseAsync(this ILockProvider provider, ILock @lock) + return provider.AcquireAsync(resource, timeUntilExpires, true, cancellationToken); + } + + public static async Task AcquireAsync(this ILockProvider provider, string resource, TimeSpan? timeUntilExpires = null, TimeSpan? acquireTimeout = null) + { + using var cancellationTokenSource = acquireTimeout.ToCancellationTokenSource(TimeSpan.FromSeconds(30)); + return await provider.AcquireAsync(resource, timeUntilExpires, true, cancellationTokenSource.Token).AnyContext(); + } + + public static async Task TryUsingAsync(this ILockProvider locker, string resource, Func work, TimeSpan? timeUntilExpires = null, CancellationToken cancellationToken = default) + { + var l = await locker.AcquireAsync(resource, timeUntilExpires, true, cancellationToken).AnyContext(); + if (l == null) + return false; + + try { - return provider.ReleaseAsync(@lock.Resource, @lock.LockId); + await work(cancellationToken).AnyContext(); } - - public static Task RenewAsync(this ILockProvider provider, ILock @lock, TimeSpan? timeUntilExpires = null) + finally { - return provider.RenewAsync(@lock.Resource, @lock.LockId, timeUntilExpires); + await l.ReleaseAsync().AnyContext(); } - public static Task AcquireAsync(this ILockProvider provider, string resource, TimeSpan? timeUntilExpires = null, CancellationToken cancellationToken = default) + return true; + } + + public static async Task TryUsingAsync(this ILockProvider locker, string resource, Func work, TimeSpan? timeUntilExpires = null, CancellationToken cancellationToken = default) + { + var l = await locker.AcquireAsync(resource, timeUntilExpires, true, cancellationToken).AnyContext(); + if (l == null) + return false; + + try { - return provider.AcquireAsync(resource, timeUntilExpires, true, cancellationToken); + await work().AnyContext(); } - - public static async Task AcquireAsync(this ILockProvider provider, string resource, TimeSpan? timeUntilExpires = null, TimeSpan? acquireTimeout = null) + finally { - using var cancellationTokenSource = acquireTimeout.ToCancellationTokenSource(TimeSpan.FromSeconds(30)); - return await provider.AcquireAsync(resource, timeUntilExpires, true, cancellationTokenSource.Token).AnyContext(); + await l.ReleaseAsync().AnyContext(); } - public static async Task TryUsingAsync(this ILockProvider locker, string resource, Func work, TimeSpan? timeUntilExpires = null, CancellationToken cancellationToken = default) - { - var l = await locker.AcquireAsync(resource, timeUntilExpires, true, cancellationToken).AnyContext(); - if (l == null) - return false; + return true; + } - try - { - await work(cancellationToken).AnyContext(); - } - finally - { - await l.ReleaseAsync().AnyContext(); - } + public static async Task TryUsingAsync(this ILockProvider locker, string resource, Func work, TimeSpan? timeUntilExpires = null, TimeSpan? acquireTimeout = null) + { + using var cancellationTokenSource = acquireTimeout.ToCancellationTokenSource(); + var l = await locker.AcquireAsync(resource, timeUntilExpires, true, cancellationTokenSource.Token).AnyContext(); + if (l == null) + return false; - return true; + try + { + await work(cancellationTokenSource.Token).AnyContext(); } - - public static async Task TryUsingAsync(this ILockProvider locker, string resource, Func work, TimeSpan? timeUntilExpires = null, CancellationToken cancellationToken = default) + finally { - var l = await locker.AcquireAsync(resource, timeUntilExpires, true, cancellationToken).AnyContext(); - if (l == null) - return false; + await l.ReleaseAsync().AnyContext(); + } - try - { - await work().AnyContext(); - } - finally - { - await l.ReleaseAsync().AnyContext(); - } + return true; + } - return true; + public static async Task TryUsingAsync(this ILockProvider locker, string resource, Func work, TimeSpan? timeUntilExpires = null, TimeSpan? acquireTimeout = null) + { + using var cancellationTokenSource = acquireTimeout.ToCancellationTokenSource(); + var l = await locker.AcquireAsync(resource, timeUntilExpires, true, cancellationTokenSource.Token).AnyContext(); + if (l == null) + return false; + + try + { + await work().AnyContext(); } + finally + { + await l.ReleaseAsync().AnyContext(); + } + + return true; + } - public static async Task TryUsingAsync(this ILockProvider locker, string resource, Func work, TimeSpan? timeUntilExpires = null, TimeSpan? acquireTimeout = null) + public static Task TryUsingAsync(this ILockProvider locker, string resource, Action work, TimeSpan? timeUntilExpires = null, TimeSpan? acquireTimeout = null) + { + return locker.TryUsingAsync(resource, () => { - using var cancellationTokenSource = acquireTimeout.ToCancellationTokenSource(); - var l = await locker.AcquireAsync(resource, timeUntilExpires, true, cancellationTokenSource.Token).AnyContext(); - if (l == null) - return false; + work(); + return Task.CompletedTask; + }, timeUntilExpires, acquireTimeout); + } - try - { - await work(cancellationTokenSource.Token).AnyContext(); - } - finally - { - await l.ReleaseAsync().AnyContext(); - } + public static async Task AcquireAsync(this ILockProvider provider, IEnumerable resources, TimeSpan? timeUntilExpires = null, bool releaseOnDispose = true, CancellationToken cancellationToken = default) + { + if (resources == null) + throw new ArgumentNullException(nameof(resources)); - return true; - } + var resourceList = resources.Distinct().ToArray(); + if (resourceList.Length == 0) + return new EmptyLock(); + + var logger = provider.GetLogger(); + + if (logger.IsEnabled(LogLevel.Trace)) + logger.LogTrace("Acquiring {LockCount} locks {Resource}", resourceList.Length, resourceList); + + var sw = Stopwatch.StartNew(); + var locks = await Task.WhenAll(resourceList.Select(r => provider.AcquireAsync(r, timeUntilExpires, releaseOnDispose, cancellationToken))); + sw.Stop(); - public static async Task TryUsingAsync(this ILockProvider locker, string resource, Func work, TimeSpan? timeUntilExpires = null, TimeSpan? acquireTimeout = null) + // if any lock is null, release any acquired and return null (all or nothing) + var acquiredLocks = locks.Where(l => l != null).ToArray(); + if (resourceList.Length > acquiredLocks.Length) { - using var cancellationTokenSource = acquireTimeout.ToCancellationTokenSource(); - var l = await locker.AcquireAsync(resource, timeUntilExpires, true, cancellationTokenSource.Token).AnyContext(); - if (l == null) - return false; + var unacquiredResources = new List(); + string[] acquiredResources = acquiredLocks.Select(l => l.Resource).ToArray(); - try + foreach (string resource in resourceList) { - await work().AnyContext(); + // account for scoped lock providers with prefixes + if (!acquiredResources.Any(r => r.EndsWith(resource))) + unacquiredResources.Add(resource); } - finally - { - await l.ReleaseAsync().AnyContext(); - } - - return true; - } - public static Task TryUsingAsync(this ILockProvider locker, string resource, Action work, TimeSpan? timeUntilExpires = null, TimeSpan? acquireTimeout = null) - { - return locker.TryUsingAsync(resource, () => + if (unacquiredResources.Count > 0) { - work(); - return Task.CompletedTask; - }, timeUntilExpires, acquireTimeout); + if (logger.IsEnabled(LogLevel.Trace)) + logger.LogTrace("Unable to acquire all {LockCount} locks {Resource} releasing acquired locks", unacquiredResources.Count, unacquiredResources); + + await Task.WhenAll(acquiredLocks.Select(l => l.ReleaseAsync())).AnyContext(); + return null; + } } - public static async Task AcquireAsync(this ILockProvider provider, IEnumerable resources, TimeSpan? timeUntilExpires = null, bool releaseOnDispose = true, CancellationToken cancellationToken = default) - { - if (resources == null) - throw new ArgumentNullException(nameof(resources)); + if (logger.IsEnabled(LogLevel.Trace)) + logger.LogTrace("Acquired {LockCount} locks {Resource} after {Duration:g}", resourceList.Length, resourceList, sw.Elapsed); - var resourceList = resources.Distinct().ToArray(); - if (resourceList.Length == 0) - return new EmptyLock(); + return new DisposableLockCollection(locks, String.Join("+", locks.Select(l => l.LockId)), sw.Elapsed, logger); + } - var logger = provider.GetLogger(); + public static async Task AcquireAsync(this ILockProvider provider, IEnumerable resources, TimeSpan? timeUntilExpires, TimeSpan? acquireTimeout) + { + using var cancellationTokenSource = acquireTimeout.ToCancellationTokenSource(TimeSpan.FromSeconds(30)); + return await provider.AcquireAsync(resources, timeUntilExpires, true, cancellationTokenSource.Token).AnyContext(); + } - if (logger.IsEnabled(LogLevel.Trace)) - logger.LogTrace("Acquiring {LockCount} locks {Resource}", resourceList.Length, resourceList); + public static async Task AcquireAsync(this ILockProvider provider, IEnumerable resources, TimeSpan? timeUntilExpires, TimeSpan? acquireTimeout, bool releaseOnDispose) + { + using var cancellationTokenSource = acquireTimeout.ToCancellationTokenSource(TimeSpan.FromSeconds(30)); + return await provider.AcquireAsync(resources, timeUntilExpires, releaseOnDispose, cancellationTokenSource.Token).AnyContext(); + } - var sw = Stopwatch.StartNew(); - var locks = await Task.WhenAll(resourceList.Select(r => provider.AcquireAsync(r, timeUntilExpires, releaseOnDispose, cancellationToken))); - sw.Stop(); + public static async Task TryUsingAsync(this ILockProvider locker, IEnumerable resources, Func work, TimeSpan? timeUntilExpires = null, CancellationToken cancellationToken = default) + { + var l = await locker.AcquireAsync(resources, timeUntilExpires, true, cancellationToken).AnyContext(); + if (l == null) + return false; - // if any lock is null, release any acquired and return null (all or nothing) - var acquiredLocks = locks.Where(l => l != null).ToArray(); - if (resourceList.Length > acquiredLocks.Length) - { - var unacquiredResources = new List(); - string[] acquiredResources = acquiredLocks.Select(l => l.Resource).ToArray(); - - foreach (string resource in resourceList) - { - // account for scoped lock providers with prefixes - if (!acquiredResources.Any(r => r.EndsWith(resource))) - unacquiredResources.Add(resource); - } - - if (unacquiredResources.Count > 0) - { - if (logger.IsEnabled(LogLevel.Trace)) - logger.LogTrace("Unable to acquire all {LockCount} locks {Resource} releasing acquired locks", unacquiredResources.Count, unacquiredResources); - - await Task.WhenAll(acquiredLocks.Select(l => l.ReleaseAsync())).AnyContext(); - return null; - } - } + try + { + await work(cancellationToken).AnyContext(); + } + finally + { + await l.ReleaseAsync().AnyContext(); + } - if (logger.IsEnabled(LogLevel.Trace)) - logger.LogTrace("Acquired {LockCount} locks {Resource} after {Duration:g}", resourceList.Length, resourceList, sw.Elapsed); + return true; + } - return new DisposableLockCollection(locks, String.Join("+", locks.Select(l => l.LockId)), sw.Elapsed, logger); - } + public static async Task TryUsingAsync(this ILockProvider locker, IEnumerable resources, Func work, TimeSpan? timeUntilExpires = null, CancellationToken cancellationToken = default) + { + var l = await locker.AcquireAsync(resources, timeUntilExpires, true, cancellationToken).AnyContext(); + if (l == null) + return false; - public static async Task AcquireAsync(this ILockProvider provider, IEnumerable resources, TimeSpan? timeUntilExpires, TimeSpan? acquireTimeout) + try { - using var cancellationTokenSource = acquireTimeout.ToCancellationTokenSource(TimeSpan.FromSeconds(30)); - return await provider.AcquireAsync(resources, timeUntilExpires, true, cancellationTokenSource.Token).AnyContext(); + await work().AnyContext(); } - - public static async Task AcquireAsync(this ILockProvider provider, IEnumerable resources, TimeSpan? timeUntilExpires, TimeSpan? acquireTimeout, bool releaseOnDispose) + finally { - using var cancellationTokenSource = acquireTimeout.ToCancellationTokenSource(TimeSpan.FromSeconds(30)); - return await provider.AcquireAsync(resources, timeUntilExpires, releaseOnDispose, cancellationTokenSource.Token).AnyContext(); + await l.ReleaseAsync().AnyContext(); } - public static async Task TryUsingAsync(this ILockProvider locker, IEnumerable resources, Func work, TimeSpan? timeUntilExpires = null, CancellationToken cancellationToken = default) - { - var l = await locker.AcquireAsync(resources, timeUntilExpires, true, cancellationToken).AnyContext(); - if (l == null) - return false; + return true; + } - try - { - await work(cancellationToken).AnyContext(); - } - finally - { - await l.ReleaseAsync().AnyContext(); - } + public static async Task TryUsingAsync(this ILockProvider locker, IEnumerable resources, Func work, TimeSpan? timeUntilExpires, TimeSpan? acquireTimeout) + { + using var cancellationTokenSource = acquireTimeout.ToCancellationTokenSource(); + var l = await locker.AcquireAsync(resources, timeUntilExpires, true, cancellationTokenSource.Token).AnyContext(); + if (l == null) + return false; - return true; + try + { + await work(cancellationTokenSource.Token).AnyContext(); } - - public static async Task TryUsingAsync(this ILockProvider locker, IEnumerable resources, Func work, TimeSpan? timeUntilExpires = null, CancellationToken cancellationToken = default) + finally { - var l = await locker.AcquireAsync(resources, timeUntilExpires, true, cancellationToken).AnyContext(); - if (l == null) - return false; - - try - { - await work().AnyContext(); - } - finally - { - await l.ReleaseAsync().AnyContext(); - } - - return true; + await l.ReleaseAsync().AnyContext(); } - public static async Task TryUsingAsync(this ILockProvider locker, IEnumerable resources, Func work, TimeSpan? timeUntilExpires, TimeSpan? acquireTimeout) - { - using var cancellationTokenSource = acquireTimeout.ToCancellationTokenSource(); - var l = await locker.AcquireAsync(resources, timeUntilExpires, true, cancellationTokenSource.Token).AnyContext(); - if (l == null) - return false; + return true; + } - try - { - await work(cancellationTokenSource.Token).AnyContext(); - } - finally - { - await l.ReleaseAsync().AnyContext(); - } + public static async Task TryUsingAsync(this ILockProvider locker, IEnumerable resources, Func work, TimeSpan? timeUntilExpires, TimeSpan? acquireTimeout) + { + using var cancellationTokenSource = acquireTimeout.ToCancellationTokenSource(); + var l = await locker.AcquireAsync(resources, timeUntilExpires, true, cancellationTokenSource.Token).AnyContext(); + if (l == null) + return false; - return true; + try + { + await work().AnyContext(); } - - public static async Task TryUsingAsync(this ILockProvider locker, IEnumerable resources, Func work, TimeSpan? timeUntilExpires, TimeSpan? acquireTimeout) + finally { - using var cancellationTokenSource = acquireTimeout.ToCancellationTokenSource(); - var l = await locker.AcquireAsync(resources, timeUntilExpires, true, cancellationTokenSource.Token).AnyContext(); - if (l == null) - return false; - - try - { - await work().AnyContext(); - } - finally - { - await l.ReleaseAsync().AnyContext(); - } - - return true; + await l.ReleaseAsync().AnyContext(); } - public static Task TryUsingAsync(this ILockProvider locker, IEnumerable resources, Action work, TimeSpan? timeUntilExpires, TimeSpan? acquireTimeout) + return true; + } + + public static Task TryUsingAsync(this ILockProvider locker, IEnumerable resources, Action work, TimeSpan? timeUntilExpires, TimeSpan? acquireTimeout) + { + return locker.TryUsingAsync(resources, () => { - return locker.TryUsingAsync(resources, () => - { - work(); - return Task.CompletedTask; - }, timeUntilExpires, acquireTimeout); - } + work(); + return Task.CompletedTask; + }, timeUntilExpires, acquireTimeout); } } diff --git a/src/Foundatio/Lock/ScopedLockProvider.cs b/src/Foundatio/Lock/ScopedLockProvider.cs index 3ab518be9..0508502d8 100644 --- a/src/Foundatio/Lock/ScopedLockProvider.cs +++ b/src/Foundatio/Lock/ScopedLockProvider.cs @@ -4,66 +4,65 @@ using Foundatio.Utility; using Microsoft.Extensions.Logging; -namespace Foundatio.Lock +namespace Foundatio.Lock; + +public class ScopedLockProvider : ILockProvider, IHaveLogger { - public class ScopedLockProvider : ILockProvider, IHaveLogger + private string _keyPrefix; + private bool _isLocked; + private readonly object _lock = new(); + + public ScopedLockProvider(ILockProvider lockProvider, string scope = null) { - private string _keyPrefix; - private bool _isLocked; - private readonly object _lock = new(); + UnscopedLockProvider = lockProvider; + _isLocked = scope != null; + Scope = !String.IsNullOrWhiteSpace(scope) ? scope.Trim() : null; - public ScopedLockProvider(ILockProvider lockProvider, string scope = null) - { - UnscopedLockProvider = lockProvider; - _isLocked = scope != null; - Scope = !String.IsNullOrWhiteSpace(scope) ? scope.Trim() : null; + _keyPrefix = Scope != null ? String.Concat(Scope, ":") : String.Empty; + } - _keyPrefix = Scope != null ? String.Concat(Scope, ":") : String.Empty; - } + public ILockProvider UnscopedLockProvider { get; } + public string Scope { get; private set; } + ILogger IHaveLogger.Logger => UnscopedLockProvider.GetLogger(); - public ILockProvider UnscopedLockProvider { get; } - public string Scope { get; private set; } - ILogger IHaveLogger.Logger => UnscopedLockProvider.GetLogger(); + public void SetScope(string scope) + { + if (_isLocked) + throw new InvalidOperationException("Scope can't be changed after it has been set"); - public void SetScope(string scope) + lock (_lock) { if (_isLocked) throw new InvalidOperationException("Scope can't be changed after it has been set"); - lock (_lock) - { - if (_isLocked) - throw new InvalidOperationException("Scope can't be changed after it has been set"); - - _isLocked = true; - Scope = !String.IsNullOrWhiteSpace(scope) ? scope.Trim() : null; - _keyPrefix = Scope != null ? String.Concat(Scope, ":") : String.Empty; - } + _isLocked = true; + Scope = !String.IsNullOrWhiteSpace(scope) ? scope.Trim() : null; + _keyPrefix = Scope != null ? String.Concat(Scope, ":") : String.Empty; } + } - protected string GetScopedLockProviderKey(string key) - { - return String.Concat(_keyPrefix, key); - } + protected string GetScopedLockProviderKey(string key) + { + return String.Concat(_keyPrefix, key); + } - public Task AcquireAsync(string resource, TimeSpan? timeUntilExpires = null, bool releaseOnDispose = true, CancellationToken cancellationToken = default) - { - return UnscopedLockProvider.AcquireAsync(GetScopedLockProviderKey(resource), timeUntilExpires, releaseOnDispose, cancellationToken); - } + public Task AcquireAsync(string resource, TimeSpan? timeUntilExpires = null, bool releaseOnDispose = true, CancellationToken cancellationToken = default) + { + return UnscopedLockProvider.AcquireAsync(GetScopedLockProviderKey(resource), timeUntilExpires, releaseOnDispose, cancellationToken); + } - public Task IsLockedAsync(string resource) - { - return UnscopedLockProvider.IsLockedAsync(GetScopedLockProviderKey(resource)); - } + public Task IsLockedAsync(string resource) + { + return UnscopedLockProvider.IsLockedAsync(GetScopedLockProviderKey(resource)); + } - public Task ReleaseAsync(string resource, string lockId) - { - return UnscopedLockProvider.ReleaseAsync(resource, lockId); - } + public Task ReleaseAsync(string resource, string lockId) + { + return UnscopedLockProvider.ReleaseAsync(resource, lockId); + } - public Task RenewAsync(string resource, string lockId, TimeSpan? timeUntilExpires = null) - { - return UnscopedLockProvider.RenewAsync(resource, lockId, timeUntilExpires); - } + public Task RenewAsync(string resource, string lockId, TimeSpan? timeUntilExpires = null) + { + return UnscopedLockProvider.RenewAsync(resource, lockId, timeUntilExpires); } } diff --git a/src/Foundatio/Lock/ThrottlingLockProvider.cs b/src/Foundatio/Lock/ThrottlingLockProvider.cs index 79618a707..c62699c2d 100644 --- a/src/Foundatio/Lock/ThrottlingLockProvider.cs +++ b/src/Foundatio/Lock/ThrottlingLockProvider.cs @@ -7,133 +7,132 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -namespace Foundatio.Lock +namespace Foundatio.Lock; + +public class ThrottlingLockProvider : ILockProvider, IHaveLogger { - public class ThrottlingLockProvider : ILockProvider, IHaveLogger + private readonly ICacheClient _cacheClient; + private readonly TimeSpan _throttlingPeriod = TimeSpan.FromMinutes(15); + private readonly int _maxHitsPerPeriod; + private readonly ILogger _logger; + + public ThrottlingLockProvider(ICacheClient cacheClient, int maxHitsPerPeriod = 100, TimeSpan? throttlingPeriod = null, ILoggerFactory loggerFactory = null) { - private readonly ICacheClient _cacheClient; - private readonly TimeSpan _throttlingPeriod = TimeSpan.FromMinutes(15); - private readonly int _maxHitsPerPeriod; - private readonly ILogger _logger; + _logger = loggerFactory?.CreateLogger() ?? NullLogger.Instance; + _cacheClient = new ScopedCacheClient(cacheClient, "lock:throttled"); + _maxHitsPerPeriod = maxHitsPerPeriod; - public ThrottlingLockProvider(ICacheClient cacheClient, int maxHitsPerPeriod = 100, TimeSpan? throttlingPeriod = null, ILoggerFactory loggerFactory = null) - { - _logger = loggerFactory?.CreateLogger() ?? NullLogger.Instance; - _cacheClient = new ScopedCacheClient(cacheClient, "lock:throttled"); - _maxHitsPerPeriod = maxHitsPerPeriod; + if (maxHitsPerPeriod <= 0) + throw new ArgumentException("Must be a positive number.", nameof(maxHitsPerPeriod)); - if (maxHitsPerPeriod <= 0) - throw new ArgumentException("Must be a positive number.", nameof(maxHitsPerPeriod)); + if (throttlingPeriod.HasValue) + _throttlingPeriod = throttlingPeriod.Value; + } - if (throttlingPeriod.HasValue) - _throttlingPeriod = throttlingPeriod.Value; - } + ILogger IHaveLogger.Logger => _logger; - ILogger IHaveLogger.Logger => _logger; + public async Task AcquireAsync(string resource, TimeSpan? timeUntilExpires = null, bool releaseOnDispose = true, CancellationToken cancellationToken = default) + { + bool isTraceLogLevelEnabled = _logger.IsEnabled(LogLevel.Trace); + if (isTraceLogLevelEnabled) _logger.LogTrace("AcquireLockAsync: {Resource}", resource); - public async Task AcquireAsync(string resource, TimeSpan? timeUntilExpires = null, bool releaseOnDispose = true, CancellationToken cancellationToken = default) - { - bool isTraceLogLevelEnabled = _logger.IsEnabled(LogLevel.Trace); - if (isTraceLogLevelEnabled) _logger.LogTrace("AcquireLockAsync: {Resource}", resource); + bool allowLock = false; + byte errors = 0; - bool allowLock = false; - byte errors = 0; + string lockId = Guid.NewGuid().ToString("N"); + var sw = Stopwatch.StartNew(); + do + { + string cacheKey = GetCacheKey(resource, SystemClock.UtcNow); - string lockId = Guid.NewGuid().ToString("N"); - var sw = Stopwatch.StartNew(); - do + try { - string cacheKey = GetCacheKey(resource, SystemClock.UtcNow); + if (isTraceLogLevelEnabled) + _logger.LogTrace("Current time: {CurrentTime} throttle: {ThrottlingPeriod} key: {Key}", SystemClock.UtcNow.ToString("mm:ss.fff"), SystemClock.UtcNow.Floor(_throttlingPeriod).ToString("mm:ss.fff"), cacheKey); + var hitCount = await _cacheClient.GetAsync(cacheKey, 0).AnyContext(); - try + if (isTraceLogLevelEnabled) + _logger.LogTrace("Current hit count: {HitCount} max: {MaxHitsPerPeriod}", hitCount, _maxHitsPerPeriod); + if (hitCount <= _maxHitsPerPeriod - 1) { - if (isTraceLogLevelEnabled) - _logger.LogTrace("Current time: {CurrentTime} throttle: {ThrottlingPeriod} key: {Key}", SystemClock.UtcNow.ToString("mm:ss.fff"), SystemClock.UtcNow.Floor(_throttlingPeriod).ToString("mm:ss.fff"), cacheKey); - var hitCount = await _cacheClient.GetAsync(cacheKey, 0).AnyContext(); - - if (isTraceLogLevelEnabled) - _logger.LogTrace("Current hit count: {HitCount} max: {MaxHitsPerPeriod}", hitCount, _maxHitsPerPeriod); - if (hitCount <= _maxHitsPerPeriod - 1) - { - hitCount = await _cacheClient.IncrementAsync(cacheKey, 1, SystemClock.UtcNow.Ceiling(_throttlingPeriod)).AnyContext(); - - // make sure someone didn't beat us to it. - if (hitCount <= _maxHitsPerPeriod) - { - allowLock = true; - break; - } + hitCount = await _cacheClient.IncrementAsync(cacheKey, 1, SystemClock.UtcNow.Ceiling(_throttlingPeriod)).AnyContext(); - if (isTraceLogLevelEnabled) _logger.LogTrace("Max hits exceeded after increment for {Resource}.", resource); - } - else if (isTraceLogLevelEnabled) + // make sure someone didn't beat us to it. + if (hitCount <= _maxHitsPerPeriod) { - _logger.LogTrace("Max hits exceeded for {Resource}.", resource); - } - - if (cancellationToken.IsCancellationRequested) + allowLock = true; break; - - var sleepUntil = SystemClock.UtcNow.Ceiling(_throttlingPeriod).AddMilliseconds(1); - if (sleepUntil > SystemClock.UtcNow) - { - if (isTraceLogLevelEnabled) _logger.LogTrace("Sleeping until key expires: {SleepUntil}", sleepUntil - SystemClock.UtcNow); - await SystemClock.SleepAsync(sleepUntil - SystemClock.UtcNow, cancellationToken).AnyContext(); - } - else - { - if (isTraceLogLevelEnabled) _logger.LogTrace("Default sleep"); - await SystemClock.SleepAsync(50, cancellationToken).AnyContext(); } + + if (isTraceLogLevelEnabled) _logger.LogTrace("Max hits exceeded after increment for {Resource}.", resource); } - catch (OperationCanceledException) + else if (isTraceLogLevelEnabled) { - return null; + _logger.LogTrace("Max hits exceeded for {Resource}.", resource); } - catch (Exception ex) - { - _logger.LogError(ex, "Error acquiring throttled lock: name={Resource} message={Message}", resource, ex.Message); - errors++; - if (errors >= 3) - break; - await SystemClock.SleepSafeAsync(50, cancellationToken).AnyContext(); + if (cancellationToken.IsCancellationRequested) + break; + + var sleepUntil = SystemClock.UtcNow.Ceiling(_throttlingPeriod).AddMilliseconds(1); + if (sleepUntil > SystemClock.UtcNow) + { + if (isTraceLogLevelEnabled) _logger.LogTrace("Sleeping until key expires: {SleepUntil}", sleepUntil - SystemClock.UtcNow); + await SystemClock.SleepAsync(sleepUntil - SystemClock.UtcNow, cancellationToken).AnyContext(); + } + else + { + if (isTraceLogLevelEnabled) _logger.LogTrace("Default sleep"); + await SystemClock.SleepAsync(50, cancellationToken).AnyContext(); } - } while (!cancellationToken.IsCancellationRequested); + } + catch (OperationCanceledException) + { + return null; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error acquiring throttled lock: name={Resource} message={Message}", resource, ex.Message); + errors++; + if (errors >= 3) + break; - if (cancellationToken.IsCancellationRequested && isTraceLogLevelEnabled) - _logger.LogTrace("Cancellation requested"); + await SystemClock.SleepSafeAsync(50, cancellationToken).AnyContext(); + } + } while (!cancellationToken.IsCancellationRequested); - if (!allowLock) - return null; + if (cancellationToken.IsCancellationRequested && isTraceLogLevelEnabled) + _logger.LogTrace("Cancellation requested"); - if (isTraceLogLevelEnabled) - _logger.LogTrace("Allowing lock: {Resource}", resource); + if (!allowLock) + return null; - sw.Stop(); - return new DisposableLock(resource, lockId, sw.Elapsed, this, _logger, releaseOnDispose); - } + if (isTraceLogLevelEnabled) + _logger.LogTrace("Allowing lock: {Resource}", resource); - public async Task IsLockedAsync(string resource) - { - string cacheKey = GetCacheKey(resource, SystemClock.UtcNow); - long hitCount = await _cacheClient.GetAsync(cacheKey, 0).AnyContext(); - return hitCount >= _maxHitsPerPeriod; - } + sw.Stop(); + return new DisposableLock(resource, lockId, sw.Elapsed, this, _logger, releaseOnDispose); + } - public Task ReleaseAsync(string resource, string lockId) - { - return Task.CompletedTask; - } + public async Task IsLockedAsync(string resource) + { + string cacheKey = GetCacheKey(resource, SystemClock.UtcNow); + long hitCount = await _cacheClient.GetAsync(cacheKey, 0).AnyContext(); + return hitCount >= _maxHitsPerPeriod; + } - public Task RenewAsync(string resource, string lockId, TimeSpan? timeUntilExpires = null) - { - return Task.CompletedTask; - } + public Task ReleaseAsync(string resource, string lockId) + { + return Task.CompletedTask; + } - private string GetCacheKey(string resource, DateTime now) - { - return String.Concat(resource, ":", now.Floor(_throttlingPeriod).Ticks); - } + public Task RenewAsync(string resource, string lockId, TimeSpan? timeUntilExpires = null) + { + return Task.CompletedTask; + } + + private string GetCacheKey(string resource, DateTime now) + { + return String.Concat(resource, ":", now.Floor(_throttlingPeriod).Ticks); } } diff --git a/src/Foundatio/Messaging/IMessageBus.cs b/src/Foundatio/Messaging/IMessageBus.cs index 3af63a204..255316d1b 100644 --- a/src/Foundatio/Messaging/IMessageBus.cs +++ b/src/Foundatio/Messaging/IMessageBus.cs @@ -1,15 +1,14 @@ using System; using System.Collections.Generic; -namespace Foundatio.Messaging -{ - public interface IMessageBus : IMessagePublisher, IMessageSubscriber, IDisposable { } +namespace Foundatio.Messaging; + +public interface IMessageBus : IMessagePublisher, IMessageSubscriber, IDisposable { } - public class MessageOptions - { - public string UniqueId { get; set; } - public string CorrelationId { get; set; } - public TimeSpan? DeliveryDelay { get; set; } - public IDictionary Properties { get; set; } = new Dictionary(); - } +public class MessageOptions +{ + public string UniqueId { get; set; } + public string CorrelationId { get; set; } + public TimeSpan? DeliveryDelay { get; set; } + public IDictionary Properties { get; set; } = new Dictionary(); } diff --git a/src/Foundatio/Messaging/IMessagePublisher.cs b/src/Foundatio/Messaging/IMessagePublisher.cs index 534a6786d..66f692d22 100644 --- a/src/Foundatio/Messaging/IMessagePublisher.cs +++ b/src/Foundatio/Messaging/IMessagePublisher.cs @@ -2,23 +2,22 @@ using System.Threading; using System.Threading.Tasks; -namespace Foundatio.Messaging +namespace Foundatio.Messaging; + +public interface IMessagePublisher { - public interface IMessagePublisher + Task PublishAsync(Type messageType, object message, MessageOptions options = null, CancellationToken cancellationToken = default); +} + +public static class MessagePublisherExtensions +{ + public static Task PublishAsync(this IMessagePublisher publisher, T message, MessageOptions options = null) where T : class { - Task PublishAsync(Type messageType, object message, MessageOptions options = null, CancellationToken cancellationToken = default); + return publisher.PublishAsync(typeof(T), message, options); } - public static class MessagePublisherExtensions + public static Task PublishAsync(this IMessagePublisher publisher, T message, TimeSpan delay, CancellationToken cancellationToken = default) where T : class { - public static Task PublishAsync(this IMessagePublisher publisher, T message, MessageOptions options = null) where T : class - { - return publisher.PublishAsync(typeof(T), message, options); - } - - public static Task PublishAsync(this IMessagePublisher publisher, T message, TimeSpan delay, CancellationToken cancellationToken = default) where T : class - { - return publisher.PublishAsync(typeof(T), message, new MessageOptions { DeliveryDelay = delay }, cancellationToken); - } + return publisher.PublishAsync(typeof(T), message, new MessageOptions { DeliveryDelay = delay }, cancellationToken); } } diff --git a/src/Foundatio/Messaging/IMessageSubscriber.cs b/src/Foundatio/Messaging/IMessageSubscriber.cs index b8c56bb70..f6427047f 100644 --- a/src/Foundatio/Messaging/IMessageSubscriber.cs +++ b/src/Foundatio/Messaging/IMessageSubscriber.cs @@ -2,46 +2,45 @@ using System.Threading; using System.Threading.Tasks; -namespace Foundatio.Messaging +namespace Foundatio.Messaging; + +public interface IMessageSubscriber +{ + Task SubscribeAsync(Func handler, CancellationToken cancellationToken = default) where T : class; +} + +public static class MessageBusExtensions { - public interface IMessageSubscriber + public static Task SubscribeAsync(this IMessageSubscriber subscriber, Func handler, CancellationToken cancellationToken = default) where T : class { - Task SubscribeAsync(Func handler, CancellationToken cancellationToken = default) where T : class; + return subscriber.SubscribeAsync((msg, token) => handler(msg), cancellationToken); } - public static class MessageBusExtensions + public static Task SubscribeAsync(this IMessageSubscriber subscriber, Action handler, CancellationToken cancellationToken = default) where T : class { - public static Task SubscribeAsync(this IMessageSubscriber subscriber, Func handler, CancellationToken cancellationToken = default) where T : class + return subscriber.SubscribeAsync((msg, token) => { - return subscriber.SubscribeAsync((msg, token) => handler(msg), cancellationToken); - } - - public static Task SubscribeAsync(this IMessageSubscriber subscriber, Action handler, CancellationToken cancellationToken = default) where T : class - { - return subscriber.SubscribeAsync((msg, token) => - { - handler(msg); - return Task.CompletedTask; - }, cancellationToken); - } + handler(msg); + return Task.CompletedTask; + }, cancellationToken); + } - public static Task SubscribeAsync(this IMessageSubscriber subscriber, Func handler, CancellationToken cancellationToken = default) - { - return subscriber.SubscribeAsync((msg, token) => handler(msg, token), cancellationToken); - } + public static Task SubscribeAsync(this IMessageSubscriber subscriber, Func handler, CancellationToken cancellationToken = default) + { + return subscriber.SubscribeAsync((msg, token) => handler(msg, token), cancellationToken); + } - public static Task SubscribeAsync(this IMessageSubscriber subscriber, Func handler, CancellationToken cancellationToken = default) - { - return subscriber.SubscribeAsync((msg, token) => handler(msg), cancellationToken); - } + public static Task SubscribeAsync(this IMessageSubscriber subscriber, Func handler, CancellationToken cancellationToken = default) + { + return subscriber.SubscribeAsync((msg, token) => handler(msg), cancellationToken); + } - public static Task SubscribeAsync(this IMessageSubscriber subscriber, Action handler, CancellationToken cancellationToken = default) + public static Task SubscribeAsync(this IMessageSubscriber subscriber, Action handler, CancellationToken cancellationToken = default) + { + return subscriber.SubscribeAsync((msg, token) => { - return subscriber.SubscribeAsync((msg, token) => - { - handler(msg); - return Task.CompletedTask; - }, cancellationToken); - } + handler(msg); + return Task.CompletedTask; + }, cancellationToken); } } diff --git a/src/Foundatio/Messaging/InMemoryMessageBus.cs b/src/Foundatio/Messaging/InMemoryMessageBus.cs index 46e60cb17..bf77afc2a 100644 --- a/src/Foundatio/Messaging/InMemoryMessageBus.cs +++ b/src/Foundatio/Messaging/InMemoryMessageBus.cs @@ -5,77 +5,76 @@ using Foundatio.Utility; using Microsoft.Extensions.Logging; -namespace Foundatio.Messaging +namespace Foundatio.Messaging; + +public class InMemoryMessageBus : MessageBusBase { - public class InMemoryMessageBus : MessageBusBase + private readonly ConcurrentDictionary _messageCounts = new(); + private long _messagesSent; + + public InMemoryMessageBus() : this(o => o) { } + + public InMemoryMessageBus(InMemoryMessageBusOptions options) : base(options) { } + + public InMemoryMessageBus(Builder config) + : this(config(new InMemoryMessageBusOptionsBuilder()).Build()) { } + + public long MessagesSent => _messagesSent; + + public long GetMessagesSent(Type messageType) { - private readonly ConcurrentDictionary _messageCounts = new(); - private long _messagesSent; + return _messageCounts.TryGetValue(GetMappedMessageType(messageType), out long count) ? count : 0; + } - public InMemoryMessageBus() : this(o => o) { } + public long GetMessagesSent() + { + return _messageCounts.TryGetValue(GetMappedMessageType(typeof(T)), out long count) ? count : 0; + } - public InMemoryMessageBus(InMemoryMessageBusOptions options) : base(options) { } + public void ResetMessagesSent() + { + Interlocked.Exchange(ref _messagesSent, 0); + _messageCounts.Clear(); + } - public InMemoryMessageBus(Builder config) - : this(config(new InMemoryMessageBusOptionsBuilder()).Build()) { } + protected override async Task PublishImplAsync(string messageType, object message, MessageOptions options, CancellationToken cancellationToken) + { + Interlocked.Increment(ref _messagesSent); + _messageCounts.AddOrUpdate(messageType, t => 1, (t, c) => c + 1); + var mappedType = GetMappedMessageType(messageType); - public long MessagesSent => _messagesSent; + if (_subscribers.IsEmpty) + return; - public long GetMessagesSent(Type messageType) + bool isTraceLogLevelEnabled = _logger.IsEnabled(LogLevel.Trace); + if (options.DeliveryDelay.HasValue && options.DeliveryDelay.Value > TimeSpan.Zero) { - return _messageCounts.TryGetValue(GetMappedMessageType(messageType), out long count) ? count : 0; + if (isTraceLogLevelEnabled) + _logger.LogTrace("Schedule delayed message: {MessageType} ({Delay}ms)", messageType, options.DeliveryDelay.Value.TotalMilliseconds); + SendDelayedMessage(mappedType, message, options.DeliveryDelay.Value); + return; } - public long GetMessagesSent() + byte[] body = SerializeMessageBody(messageType, message); + var messageData = new Message(body, DeserializeMessageBody) { - return _messageCounts.TryGetValue(GetMappedMessageType(typeof(T)), out long count) ? count : 0; - } + CorrelationId = options.CorrelationId, + UniqueId = options.UniqueId, + Type = messageType, + ClrType = mappedType + }; - public void ResetMessagesSent() + foreach (var property in options.Properties) + messageData.Properties[property.Key] = property.Value; + + try { - Interlocked.Exchange(ref _messagesSent, 0); - _messageCounts.Clear(); + await SendMessageToSubscribersAsync(messageData).AnyContext(); } - - protected override async Task PublishImplAsync(string messageType, object message, MessageOptions options, CancellationToken cancellationToken) + catch (Exception ex) { - Interlocked.Increment(ref _messagesSent); - _messageCounts.AddOrUpdate(messageType, t => 1, (t, c) => c + 1); - var mappedType = GetMappedMessageType(messageType); - - if (_subscribers.IsEmpty) - return; - - bool isTraceLogLevelEnabled = _logger.IsEnabled(LogLevel.Trace); - if (options.DeliveryDelay.HasValue && options.DeliveryDelay.Value > TimeSpan.Zero) - { - if (isTraceLogLevelEnabled) - _logger.LogTrace("Schedule delayed message: {MessageType} ({Delay}ms)", messageType, options.DeliveryDelay.Value.TotalMilliseconds); - SendDelayedMessage(mappedType, message, options.DeliveryDelay.Value); - return; - } - - byte[] body = SerializeMessageBody(messageType, message); - var messageData = new Message(body, DeserializeMessageBody) - { - CorrelationId = options.CorrelationId, - UniqueId = options.UniqueId, - Type = messageType, - ClrType = mappedType - }; - - foreach (var property in options.Properties) - messageData.Properties[property.Key] = property.Value; - - try - { - await SendMessageToSubscribersAsync(messageData).AnyContext(); - } - catch (Exception ex) - { - // swallow exceptions from subscriber handlers for the in memory bus - _logger.LogWarning(ex, "Error sending message to subscribers: {Message}", ex.Message); - } + // swallow exceptions from subscriber handlers for the in memory bus + _logger.LogWarning(ex, "Error sending message to subscribers: {Message}", ex.Message); } } } diff --git a/src/Foundatio/Messaging/InMemoryMessageBusOptions.cs b/src/Foundatio/Messaging/InMemoryMessageBusOptions.cs index 2049725ad..1387eee31 100644 --- a/src/Foundatio/Messaging/InMemoryMessageBusOptions.cs +++ b/src/Foundatio/Messaging/InMemoryMessageBusOptions.cs @@ -1,6 +1,5 @@ -namespace Foundatio.Messaging -{ - public class InMemoryMessageBusOptions : SharedMessageBusOptions { } +namespace Foundatio.Messaging; - public class InMemoryMessageBusOptionsBuilder : SharedMessageBusOptionsBuilder { } -} +public class InMemoryMessageBusOptions : SharedMessageBusOptions { } + +public class InMemoryMessageBusOptionsBuilder : SharedMessageBusOptionsBuilder { } diff --git a/src/Foundatio/Messaging/Message.cs b/src/Foundatio/Messaging/Message.cs index 386c4e67a..b8873321e 100644 --- a/src/Foundatio/Messaging/Message.cs +++ b/src/Foundatio/Messaging/Message.cs @@ -3,67 +3,66 @@ using System.Diagnostics; using Foundatio.Serializer; -namespace Foundatio.Messaging +namespace Foundatio.Messaging; + +public interface IMessage { - public interface IMessage - { - string UniqueId { get; } - string CorrelationId { get; } - string Type { get; } - Type ClrType { get; } - byte[] Data { get; } - object GetBody(); - IDictionary Properties { get; } - } + string UniqueId { get; } + string CorrelationId { get; } + string Type { get; } + Type ClrType { get; } + byte[] Data { get; } + object GetBody(); + IDictionary Properties { get; } +} - public interface IMessage : IMessage where T : class - { - T Body { get; } - } +public interface IMessage : IMessage where T : class +{ + T Body { get; } +} - [DebuggerDisplay("Type: {Type}")] - public class Message : IMessage +[DebuggerDisplay("Type: {Type}")] +public class Message : IMessage +{ + private readonly Func _getBody; + + public Message(byte[] data, Func getBody) { - private readonly Func _getBody; - - public Message(byte[] data, Func getBody) - { - Data = data; - _getBody = getBody; - } - - public string UniqueId { get; set; } - public string CorrelationId { get; set; } - public string Type { get; set; } - public Type ClrType { get; set; } - public IDictionary Properties { get; set; } = new Dictionary(); - public byte[] Data { get; set; } - public object GetBody() => _getBody(this); + Data = data; + _getBody = getBody; } - public class Message : IMessage where T : class - { - private readonly IMessage _message; + public string UniqueId { get; set; } + public string CorrelationId { get; set; } + public string Type { get; set; } + public Type ClrType { get; set; } + public IDictionary Properties { get; set; } = new Dictionary(); + public byte[] Data { get; set; } + public object GetBody() => _getBody(this); +} + +public class Message : IMessage where T : class +{ + private readonly IMessage _message; - public Message(IMessage message) - { - _message = message; - } + public Message(IMessage message) + { + _message = message; + } - public byte[] Data => _message.Data; + public byte[] Data => _message.Data; - public T Body => (T)GetBody(); + public T Body => (T)GetBody(); - public string UniqueId => _message.UniqueId; + public string UniqueId => _message.UniqueId; - public string CorrelationId => _message.CorrelationId; + public string CorrelationId => _message.CorrelationId; - public string Type => _message.Type; + public string Type => _message.Type; - public Type ClrType => _message.ClrType; + public Type ClrType => _message.ClrType; - public IDictionary Properties => _message.Properties; + public IDictionary Properties => _message.Properties; - public object GetBody() => _message.GetBody(); - } + public object GetBody() => _message.GetBody(); } diff --git a/src/Foundatio/Messaging/MessageBusBase.cs b/src/Foundatio/Messaging/MessageBusBase.cs index c0059b6b5..f0e1c63d6 100644 --- a/src/Foundatio/Messaging/MessageBusBase.cs +++ b/src/Foundatio/Messaging/MessageBusBase.cs @@ -11,417 +11,416 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -namespace Foundatio.Messaging +namespace Foundatio.Messaging; + +public abstract class MessageBusBase : IMessageBus, IDisposable where TOptions : SharedMessageBusOptions { - public abstract class MessageBusBase : IMessageBus, IDisposable where TOptions : SharedMessageBusOptions + private readonly CancellationTokenSource _messageBusDisposedCancellationTokenSource; + protected readonly ConcurrentDictionary _subscribers = new(); + protected readonly TOptions _options; + protected readonly ILogger _logger; + protected readonly ISerializer _serializer; + private bool _isDisposed; + + public MessageBusBase(TOptions options) { - private readonly CancellationTokenSource _messageBusDisposedCancellationTokenSource; - protected readonly ConcurrentDictionary _subscribers = new(); - protected readonly TOptions _options; - protected readonly ILogger _logger; - protected readonly ISerializer _serializer; - private bool _isDisposed; - - public MessageBusBase(TOptions options) - { - _options = options ?? throw new ArgumentNullException(nameof(options)); - var loggerFactory = options?.LoggerFactory ?? NullLoggerFactory.Instance; - _logger = loggerFactory.CreateLogger(GetType()); - _serializer = options.Serializer ?? DefaultSerializer.Instance; - MessageBusId = _options.Topic + Guid.NewGuid().ToString("N").Substring(10); - _messageBusDisposedCancellationTokenSource = new CancellationTokenSource(); - } - - protected virtual Task EnsureTopicCreatedAsync(CancellationToken cancellationToken) => Task.CompletedTask; - protected abstract Task PublishImplAsync(string messageType, object message, MessageOptions options, CancellationToken cancellationToken); - public async Task PublishAsync(Type messageType, object message, MessageOptions options = null, CancellationToken cancellationToken = default) - { - if (messageType == null || message == null) - return; + _options = options ?? throw new ArgumentNullException(nameof(options)); + var loggerFactory = options?.LoggerFactory ?? NullLoggerFactory.Instance; + _logger = loggerFactory.CreateLogger(GetType()); + _serializer = options.Serializer ?? DefaultSerializer.Instance; + MessageBusId = _options.Topic + Guid.NewGuid().ToString("N").Substring(10); + _messageBusDisposedCancellationTokenSource = new CancellationTokenSource(); + } - options ??= new MessageOptions(); + protected virtual Task EnsureTopicCreatedAsync(CancellationToken cancellationToken) => Task.CompletedTask; + protected abstract Task PublishImplAsync(string messageType, object message, MessageOptions options, CancellationToken cancellationToken); + public async Task PublishAsync(Type messageType, object message, MessageOptions options = null, CancellationToken cancellationToken = default) + { + if (messageType == null || message == null) + return; - if (String.IsNullOrEmpty(options.CorrelationId)) - { - options.CorrelationId = Activity.Current?.Id; - if (!String.IsNullOrEmpty(Activity.Current?.TraceStateString)) - options.Properties.Add("TraceState", Activity.Current.TraceStateString); - } + options ??= new MessageOptions(); - await EnsureTopicCreatedAsync(cancellationToken).AnyContext(); - await PublishImplAsync(GetMappedMessageType(messageType), message, options ?? new MessageOptions(), cancellationToken).AnyContext(); + if (String.IsNullOrEmpty(options.CorrelationId)) + { + options.CorrelationId = Activity.Current?.Id; + if (!String.IsNullOrEmpty(Activity.Current?.TraceStateString)) + options.Properties.Add("TraceState", Activity.Current.TraceStateString); } - private readonly ConcurrentDictionary _mappedMessageTypesCache = new(); - protected string GetMappedMessageType(Type messageType) + await EnsureTopicCreatedAsync(cancellationToken).AnyContext(); + await PublishImplAsync(GetMappedMessageType(messageType), message, options ?? new MessageOptions(), cancellationToken).AnyContext(); + } + + private readonly ConcurrentDictionary _mappedMessageTypesCache = new(); + protected string GetMappedMessageType(Type messageType) + { + return _mappedMessageTypesCache.GetOrAdd(messageType, type => { - return _mappedMessageTypesCache.GetOrAdd(messageType, type => - { - var reversedMap = _options.MessageTypeMappings.ToDictionary(kvp => kvp.Value, kvp => kvp.Key); - if (reversedMap.ContainsKey(type)) - return reversedMap[type]; + var reversedMap = _options.MessageTypeMappings.ToDictionary(kvp => kvp.Value, kvp => kvp.Key); + if (reversedMap.ContainsKey(type)) + return reversedMap[type]; - return String.Concat(messageType.FullName, ", ", messageType.Assembly.GetName().Name); - }); - } + return String.Concat(messageType.FullName, ", ", messageType.Assembly.GetName().Name); + }); + } + + private readonly ConcurrentDictionary _knownMessageTypesCache = new(); + protected virtual Type GetMappedMessageType(string messageType) + { + if (String.IsNullOrEmpty(messageType)) + return null; - private readonly ConcurrentDictionary _knownMessageTypesCache = new(); - protected virtual Type GetMappedMessageType(string messageType) + return _knownMessageTypesCache.GetOrAdd(messageType, type => { - if (String.IsNullOrEmpty(messageType)) - return null; + if (_options.MessageTypeMappings != null && _options.MessageTypeMappings.ContainsKey(type)) + return _options.MessageTypeMappings[type]; - return _knownMessageTypesCache.GetOrAdd(messageType, type => + try + { + return Type.GetType(type); + } + catch (Exception) { - if (_options.MessageTypeMappings != null && _options.MessageTypeMappings.ContainsKey(type)) - return _options.MessageTypeMappings[type]; - try { + string[] typeParts = type.Split(','); + if (typeParts.Length >= 2) + type = String.Join(",", typeParts[0], typeParts[1]); + + // try resolve type without version return Type.GetType(type); } - catch (Exception) + catch (Exception ex) { - try - { - string[] typeParts = type.Split(','); - if (typeParts.Length >= 2) - type = String.Join(",", typeParts[0], typeParts[1]); + if (_logger.IsEnabled(LogLevel.Warning)) + _logger.LogWarning(ex, "Error getting message body type: {MessageType}", type); - // try resolve type without version - return Type.GetType(type); - } - catch (Exception ex) - { - if (_logger.IsEnabled(LogLevel.Warning)) - _logger.LogWarning(ex, "Error getting message body type: {MessageType}", type); - - return null; - } + return null; } - }); - } + } + }); + } - protected virtual Task RemoveTopicSubscriptionAsync() => Task.CompletedTask; - protected virtual Task EnsureTopicSubscriptionAsync(CancellationToken cancellationToken) => Task.CompletedTask; + protected virtual Task RemoveTopicSubscriptionAsync() => Task.CompletedTask; + protected virtual Task EnsureTopicSubscriptionAsync(CancellationToken cancellationToken) => Task.CompletedTask; - protected virtual Task SubscribeImplAsync(Func handler, CancellationToken cancellationToken) where T : class + protected virtual Task SubscribeImplAsync(Func handler, CancellationToken cancellationToken) where T : class + { + var subscriber = new Subscriber { - var subscriber = new Subscriber + CancellationToken = cancellationToken, + Type = typeof(T), + Action = (message, token) => { - CancellationToken = cancellationToken, - Type = typeof(T), - Action = (message, token) => + if (message is not T) { - if (message is not T) - { - if (_logger.IsEnabled(LogLevel.Trace)) - _logger.LogTrace("Unable to call subscriber action: {MessageType} cannot be safely casted to {SubscriberType}", message.GetType(), typeof(T)); - return Task.CompletedTask; - } - - return handler((T)message, cancellationToken); + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.LogTrace("Unable to call subscriber action: {MessageType} cannot be safely casted to {SubscriberType}", message.GetType(), typeof(T)); + return Task.CompletedTask; } - }; - if (cancellationToken != CancellationToken.None) - { - cancellationToken.Register(() => - { - _subscribers.TryRemove(subscriber.Id, out _); - if (_subscribers.Count == 0) - RemoveTopicSubscriptionAsync().GetAwaiter().GetResult(); - }); + return handler((T)message, cancellationToken); } + }; - if (subscriber.Type.Name == "IMessage`1" && subscriber.Type.GenericTypeArguments.Length == 1) + if (cancellationToken != CancellationToken.None) + { + cancellationToken.Register(() => { - var modelType = subscriber.Type.GenericTypeArguments.Single(); - subscriber.GenericType = typeof(Message<>).MakeGenericType(modelType); - } - - if (!_subscribers.TryAdd(subscriber.Id, subscriber) && _logger.IsEnabled(LogLevel.Error)) - _logger.LogError("Unable to add subscriber {SubscriberId}", subscriber.Id); - - return Task.CompletedTask; + _subscribers.TryRemove(subscriber.Id, out _); + if (_subscribers.Count == 0) + RemoveTopicSubscriptionAsync().GetAwaiter().GetResult(); + }); } - public async Task SubscribeAsync(Func handler, CancellationToken cancellationToken = default) where T : class + if (subscriber.Type.Name == "IMessage`1" && subscriber.Type.GenericTypeArguments.Length == 1) { - if (_logger.IsEnabled(LogLevel.Trace)) - _logger.LogTrace("Adding subscriber for {MessageType}.", typeof(T).FullName); - - await SubscribeImplAsync(handler, cancellationToken).AnyContext(); - await EnsureTopicSubscriptionAsync(cancellationToken).AnyContext(); + var modelType = subscriber.Type.GenericTypeArguments.Single(); + subscriber.GenericType = typeof(Message<>).MakeGenericType(modelType); } - protected List GetMessageSubscribers(IMessage message) - { - return _subscribers.Values.Where(s => SubscriberHandlesMessage(s, message)).ToList(); - } + if (!_subscribers.TryAdd(subscriber.Id, subscriber) && _logger.IsEnabled(LogLevel.Error)) + _logger.LogError("Unable to add subscriber {SubscriberId}", subscriber.Id); - protected virtual bool SubscriberHandlesMessage(Subscriber subscriber, IMessage message) - { - if (subscriber.Type == typeof(IMessage)) - return true; + return Task.CompletedTask; + } - var clrType = message.ClrType ?? GetMappedMessageType(message.Type); - if (clrType is null) - { - if (_logger.IsEnabled(LogLevel.Warning)) - _logger.LogWarning("Unable to resolve CLR type for message body type: ClrType={MessageClrType} Type={MessageType}", message.ClrType, message.Type); + public async Task SubscribeAsync(Func handler, CancellationToken cancellationToken = default) where T : class + { + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.LogTrace("Adding subscriber for {MessageType}.", typeof(T).FullName); - return false; - } + await SubscribeImplAsync(handler, cancellationToken).AnyContext(); + await EnsureTopicSubscriptionAsync(cancellationToken).AnyContext(); + } + + protected List GetMessageSubscribers(IMessage message) + { + return _subscribers.Values.Where(s => SubscriberHandlesMessage(s, message)).ToList(); + } - if (subscriber.IsAssignableFrom(clrType)) - return true; + protected virtual bool SubscriberHandlesMessage(Subscriber subscriber, IMessage message) + { + if (subscriber.Type == typeof(IMessage)) + return true; + + var clrType = message.ClrType ?? GetMappedMessageType(message.Type); + if (clrType is null) + { + if (_logger.IsEnabled(LogLevel.Warning)) + _logger.LogWarning("Unable to resolve CLR type for message body type: ClrType={MessageClrType} Type={MessageType}", message.ClrType, message.Type); return false; } - protected virtual byte[] SerializeMessageBody(string messageType, object body) + if (subscriber.IsAssignableFrom(clrType)) + return true; + + return false; + } + + protected virtual byte[] SerializeMessageBody(string messageType, object body) + { + if (body == null) + return Array.Empty(); + + return _serializer.SerializeToBytes(body); + } + + protected virtual object DeserializeMessageBody(IMessage message) + { + if (message.Data is null || message.Data.Length == 0) + return null; + + object body; + try { - if (body == null) - return Array.Empty(); + var clrType = message.ClrType ?? GetMappedMessageType(message.Type); + body = clrType != null ? _serializer.Deserialize(message.Data, clrType) : message.Data; + } + catch (Exception ex) + { + if (_logger.IsEnabled(LogLevel.Error)) + _logger.LogError(ex, "Error deserializing message body: {Message}", ex.Message); - return _serializer.SerializeToBytes(body); + return null; } - protected virtual object DeserializeMessageBody(IMessage message) - { - if (message.Data is null || message.Data.Length == 0) - return null; + return body; + } - object body; - try - { - var clrType = message.ClrType ?? GetMappedMessageType(message.Type); - body = clrType != null ? _serializer.Deserialize(message.Data, clrType) : message.Data; - } - catch (Exception ex) - { - if (_logger.IsEnabled(LogLevel.Error)) - _logger.LogError(ex, "Error deserializing message body: {Message}", ex.Message); + protected async Task SendMessageToSubscribersAsync(IMessage message) + { + bool isTraceLogLevelEnabled = _logger.IsEnabled(LogLevel.Trace); + var subscribers = GetMessageSubscribers(message); - return null; - } + if (isTraceLogLevelEnabled) + _logger.LogTrace("Found {SubscriberCount} subscribers for message type: ClrType={MessageClrType} Type={MessageType}", subscribers.Count, message.ClrType, message.Type); - return body; - } + if (subscribers.Count == 0) + return; - protected async Task SendMessageToSubscribersAsync(IMessage message) + var subscriberHandlers = subscribers.Select(subscriber => { - bool isTraceLogLevelEnabled = _logger.IsEnabled(LogLevel.Trace); - var subscribers = GetMessageSubscribers(message); - - if (isTraceLogLevelEnabled) - _logger.LogTrace("Found {SubscriberCount} subscribers for message type: ClrType={MessageClrType} Type={MessageType}", subscribers.Count, message.ClrType, message.Type); + if (subscriber.CancellationToken.IsCancellationRequested) + { + if (_subscribers.TryRemove(subscriber.Id, out _)) + { + if (isTraceLogLevelEnabled) + _logger.LogTrace("Removed cancelled subscriber: {SubscriberId}", subscriber.Id); + } + else if (isTraceLogLevelEnabled) + { + _logger.LogTrace("Unable to remove cancelled subscriber: {SubscriberId}", subscriber.Id); + } - if (subscribers.Count == 0) - return; + return Task.CompletedTask; + } - var subscriberHandlers = subscribers.Select(subscriber => + return Task.Run(async () => { if (subscriber.CancellationToken.IsCancellationRequested) { - if (_subscribers.TryRemove(subscriber.Id, out _)) - { - if (isTraceLogLevelEnabled) - _logger.LogTrace("Removed cancelled subscriber: {SubscriberId}", subscriber.Id); - } - else if (isTraceLogLevelEnabled) - { - _logger.LogTrace("Unable to remove cancelled subscriber: {SubscriberId}", subscriber.Id); - } + if (isTraceLogLevelEnabled) + _logger.LogTrace("The cancelled subscriber action will not be called: {SubscriberId}", subscriber.Id); - return Task.CompletedTask; + return; } - return Task.Run(async () => - { - if (subscriber.CancellationToken.IsCancellationRequested) - { - if (isTraceLogLevelEnabled) - _logger.LogTrace("The cancelled subscriber action will not be called: {SubscriberId}", subscriber.Id); + if (isTraceLogLevelEnabled) + _logger.LogTrace("Calling subscriber action: {SubscriberId}", subscriber.Id); - return; - } + using var activity = StartHandleMessageActivity(message); - if (isTraceLogLevelEnabled) - _logger.LogTrace("Calling subscriber action: {SubscriberId}", subscriber.Id); - - using var activity = StartHandleMessageActivity(message); + using (_logger.BeginScope(s => s + .PropertyIf("UniqueId", message.UniqueId, !String.IsNullOrEmpty(message.UniqueId)) + .PropertyIf("CorrelationId", message.CorrelationId, !String.IsNullOrEmpty(message.CorrelationId)))) + { - using (_logger.BeginScope(s => s - .PropertyIf("UniqueId", message.UniqueId, !String.IsNullOrEmpty(message.UniqueId)) - .PropertyIf("CorrelationId", message.CorrelationId, !String.IsNullOrEmpty(message.CorrelationId)))) + if (subscriber.Type == typeof(IMessage)) { - - if (subscriber.Type == typeof(IMessage)) - { - await subscriber.Action(message, subscriber.CancellationToken).AnyContext(); - } - else if (subscriber.GenericType != null) - { - object typedMessage = Activator.CreateInstance(subscriber.GenericType, message); - await subscriber.Action(typedMessage, subscriber.CancellationToken).AnyContext(); - } - else - { - await subscriber.Action(message.GetBody(), subscriber.CancellationToken).AnyContext(); - } + await subscriber.Action(message, subscriber.CancellationToken).AnyContext(); + } + else if (subscriber.GenericType != null) + { + object typedMessage = Activator.CreateInstance(subscriber.GenericType, message); + await subscriber.Action(typedMessage, subscriber.CancellationToken).AnyContext(); } + else + { + await subscriber.Action(message.GetBody(), subscriber.CancellationToken).AnyContext(); + } + } - if (isTraceLogLevelEnabled) - _logger.LogTrace("Finished calling subscriber action: {SubscriberId}", subscriber.Id); - }); + if (isTraceLogLevelEnabled) + _logger.LogTrace("Finished calling subscriber action: {SubscriberId}", subscriber.Id); }); + }); - try - { - await Task.WhenAll(subscriberHandlers.ToArray()); - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Error sending message to subscribers: {Message}", ex.Message); - - throw; - } - - if (isTraceLogLevelEnabled) - _logger.LogTrace("Done enqueueing message to {SubscriberCount} subscribers for message type {MessageType}", subscribers.Count, message.Type); + try + { + await Task.WhenAll(subscriberHandlers.ToArray()); } - - protected virtual Activity StartHandleMessageActivity(IMessage message) + catch (Exception ex) { - var activity = FoundatioDiagnostics.ActivitySource.StartActivity("HandleMessage", ActivityKind.Server, message.CorrelationId); - - if (activity == null) - return activity; + _logger.LogWarning(ex, "Error sending message to subscribers: {Message}", ex.Message); - if (message.Properties != null && message.Properties.TryGetValue("TraceState", out var traceState)) - activity.TraceStateString = traceState.ToString(); + throw; + } - activity.DisplayName = $"Message: {message.ClrType?.Name ?? message.Type}"; + if (isTraceLogLevelEnabled) + _logger.LogTrace("Done enqueueing message to {SubscriberCount} subscribers for message type {MessageType}", subscribers.Count, message.Type); + } - EnrichHandleMessageActivity(activity, message); + protected virtual Activity StartHandleMessageActivity(IMessage message) + { + var activity = FoundatioDiagnostics.ActivitySource.StartActivity("HandleMessage", ActivityKind.Server, message.CorrelationId); + if (activity == null) return activity; - } - protected virtual void EnrichHandleMessageActivity(Activity activity, IMessage message) - { - if (!activity.IsAllDataRequested) - return; + if (message.Properties != null && message.Properties.TryGetValue("TraceState", out var traceState)) + activity.TraceStateString = traceState.ToString(); - activity.AddTag("MessageType", message.Type); - activity.AddTag("ClrType", message.ClrType?.FullName); - activity.AddTag("UniqueId", message.UniqueId); - activity.AddTag("CorrelationId", message.CorrelationId); + activity.DisplayName = $"Message: {message.ClrType?.Name ?? message.Type}"; - if (message.Properties == null || message.Properties.Count <= 0) - return; + EnrichHandleMessageActivity(activity, message); - foreach (var p in message.Properties) - { - if (p.Key != "TraceState") - activity.AddTag(p.Key, p.Value); - } - } + return activity; + } - protected Task AddDelayedMessageAsync(Type messageType, object message, TimeSpan delay) - { - if (message == null) - throw new ArgumentNullException(nameof(message)); + protected virtual void EnrichHandleMessageActivity(Activity activity, IMessage message) + { + if (!activity.IsAllDataRequested) + return; - SendDelayedMessage(messageType, message, delay); + activity.AddTag("MessageType", message.Type); + activity.AddTag("ClrType", message.ClrType?.FullName); + activity.AddTag("UniqueId", message.UniqueId); + activity.AddTag("CorrelationId", message.CorrelationId); - return Task.CompletedTask; - } + if (message.Properties == null || message.Properties.Count <= 0) + return; - protected void SendDelayedMessage(Type messageType, object message, TimeSpan delay) + foreach (var p in message.Properties) { - if (message == null) - throw new ArgumentNullException(nameof(message)); - - if (delay <= TimeSpan.Zero) - throw new ArgumentOutOfRangeException(nameof(delay)); + if (p.Key != "TraceState") + activity.AddTag(p.Key, p.Value); + } + } - var sendTime = SystemClock.UtcNow.SafeAdd(delay); - Task.Factory.StartNew(async () => - { - await SystemClock.SleepSafeAsync(delay, _messageBusDisposedCancellationTokenSource.Token).AnyContext(); + protected Task AddDelayedMessageAsync(Type messageType, object message, TimeSpan delay) + { + if (message == null) + throw new ArgumentNullException(nameof(message)); - bool isTraceLevelEnabled = _logger.IsEnabled(LogLevel.Trace); - if (_messageBusDisposedCancellationTokenSource.IsCancellationRequested) - { - if (isTraceLevelEnabled) - _logger.LogTrace("Discarding delayed message scheduled for {SendTime:O} for type {MessageType}", sendTime, messageType); - return; - } + SendDelayedMessage(messageType, message, delay); - if (isTraceLevelEnabled) - _logger.LogTrace("Sending delayed message scheduled for {SendTime:O} for type {MessageType}", sendTime, messageType); + return Task.CompletedTask; + } - await PublishAsync(messageType, message).AnyContext(); - }); - } + protected void SendDelayedMessage(Type messageType, object message, TimeSpan delay) + { + if (message == null) + throw new ArgumentNullException(nameof(message)); - public string MessageBusId { get; protected set; } + if (delay <= TimeSpan.Zero) + throw new ArgumentOutOfRangeException(nameof(delay)); - public virtual void Dispose() + var sendTime = SystemClock.UtcNow.SafeAdd(delay); + Task.Factory.StartNew(async () => { - if (_isDisposed) + await SystemClock.SleepSafeAsync(delay, _messageBusDisposedCancellationTokenSource.Token).AnyContext(); + + bool isTraceLevelEnabled = _logger.IsEnabled(LogLevel.Trace); + if (_messageBusDisposedCancellationTokenSource.IsCancellationRequested) { - _logger.LogTrace("MessageBus {0} dispose was already called.", MessageBusId); + if (isTraceLevelEnabled) + _logger.LogTrace("Discarding delayed message scheduled for {SendTime:O} for type {MessageType}", sendTime, messageType); return; } - _isDisposed = true; + if (isTraceLevelEnabled) + _logger.LogTrace("Sending delayed message scheduled for {SendTime:O} for type {MessageType}", sendTime, messageType); - _logger.LogTrace("MessageBus {0} dispose", MessageBusId); - _subscribers?.Clear(); - _messageBusDisposedCancellationTokenSource?.Cancel(); - _messageBusDisposedCancellationTokenSource?.Dispose(); - } + await PublishAsync(messageType, message).AnyContext(); + }); + } + + public string MessageBusId { get; protected set; } - [DebuggerDisplay("MessageType: {MessageType} SendTime: {SendTime} Message: {Message}")] - protected class DelayedMessage + public virtual void Dispose() + { + if (_isDisposed) { - public DateTime SendTime { get; set; } - public Type MessageType { get; set; } - public object Message { get; set; } + _logger.LogTrace("MessageBus {0} dispose was already called.", MessageBusId); + return; } - [DebuggerDisplay("Id: {Id} Type: {Type} CancellationToken: {CancellationToken}")] - protected class Subscriber - { - private readonly ConcurrentDictionary _assignableTypesCache = new(); + _isDisposed = true; - public string Id { get; private set; } = Guid.NewGuid().ToString("N"); - public CancellationToken CancellationToken { get; set; } - public Type Type { get; set; } - public Type GenericType { get; set; } - public Func Action { get; set; } + _logger.LogTrace("MessageBus {0} dispose", MessageBusId); + _subscribers?.Clear(); + _messageBusDisposedCancellationTokenSource?.Cancel(); + _messageBusDisposedCancellationTokenSource?.Dispose(); + } - public bool IsAssignableFrom(Type type) - { - if (type is null) - return false; + [DebuggerDisplay("MessageType: {MessageType} SendTime: {SendTime} Message: {Message}")] + protected class DelayedMessage + { + public DateTime SendTime { get; set; } + public Type MessageType { get; set; } + public object Message { get; set; } + } + + [DebuggerDisplay("Id: {Id} Type: {Type} CancellationToken: {CancellationToken}")] + protected class Subscriber + { + private readonly ConcurrentDictionary _assignableTypesCache = new(); - return _assignableTypesCache.GetOrAdd(type, t => + public string Id { get; private set; } = Guid.NewGuid().ToString("N"); + public CancellationToken CancellationToken { get; set; } + public Type Type { get; set; } + public Type GenericType { get; set; } + public Func Action { get; set; } + + public bool IsAssignableFrom(Type type) + { + if (type is null) + return false; + + return _assignableTypesCache.GetOrAdd(type, t => + { + if (t.IsClass) { - if (t.IsClass) - { - var typedMessageType = typeof(IMessage<>).MakeGenericType(t); - if (Type == typedMessageType) - return true; - } + var typedMessageType = typeof(IMessage<>).MakeGenericType(t); + if (Type == typedMessageType) + return true; + } - return Type.GetTypeInfo().IsAssignableFrom(t); - }); - } + return Type.GetTypeInfo().IsAssignableFrom(t); + }); } } } diff --git a/src/Foundatio/Messaging/NullMessageBus.cs b/src/Foundatio/Messaging/NullMessageBus.cs index bab93f414..841ec66d2 100644 --- a/src/Foundatio/Messaging/NullMessageBus.cs +++ b/src/Foundatio/Messaging/NullMessageBus.cs @@ -20,4 +20,3 @@ public Task SubscribeAsync(Func handler, Cancella public void Dispose() { } } -} diff --git a/src/Foundatio/Messaging/SharedMessageBusOptions.cs b/src/Foundatio/Messaging/SharedMessageBusOptions.cs index 24dea2bdd..4faee1f31 100644 --- a/src/Foundatio/Messaging/SharedMessageBusOptions.cs +++ b/src/Foundatio/Messaging/SharedMessageBusOptions.cs @@ -1,49 +1,48 @@ using System; using System.Collections.Generic; -namespace Foundatio.Messaging +namespace Foundatio.Messaging; + +public class SharedMessageBusOptions : SharedOptions +{ + /// + /// The topic name + /// + public string Topic { get; set; } = "messages"; + + /// + /// Controls which types messages are mapped to. + /// + public Dictionary MessageTypeMappings { get; set; } = new Dictionary(); +} + +public class SharedMessageBusOptionsBuilder : SharedOptionsBuilder + where TOptions : SharedMessageBusOptions, new() + where TBuilder : SharedMessageBusOptionsBuilder { - public class SharedMessageBusOptions : SharedOptions + public TBuilder Topic(string topic) { - /// - /// The topic name - /// - public string Topic { get; set; } = "messages"; - - /// - /// Controls which types messages are mapped to. - /// - public Dictionary MessageTypeMappings { get; set; } = new Dictionary(); + if (string.IsNullOrEmpty(topic)) + throw new ArgumentNullException(nameof(topic)); + Target.Topic = topic; + return (TBuilder)this; } - public class SharedMessageBusOptionsBuilder : SharedOptionsBuilder - where TOptions : SharedMessageBusOptions, new() - where TBuilder : SharedMessageBusOptionsBuilder + public TBuilder MapMessageType(string name) { - public TBuilder Topic(string topic) - { - if (string.IsNullOrEmpty(topic)) - throw new ArgumentNullException(nameof(topic)); - Target.Topic = topic; - return (TBuilder)this; - } - - public TBuilder MapMessageType(string name) - { - if (Target.MessageTypeMappings == null) - Target.MessageTypeMappings = new Dictionary(); - - Target.MessageTypeMappings[name] = typeof(T); - return (TBuilder)this; - } - - public TBuilder MapMessageTypeToClassName() - { - if (Target.MessageTypeMappings == null) - Target.MessageTypeMappings = new Dictionary(); - - Target.MessageTypeMappings[typeof(T).Name] = typeof(T); - return (TBuilder)this; - } + if (Target.MessageTypeMappings == null) + Target.MessageTypeMappings = new Dictionary(); + + Target.MessageTypeMappings[name] = typeof(T); + return (TBuilder)this; + } + + public TBuilder MapMessageTypeToClassName() + { + if (Target.MessageTypeMappings == null) + Target.MessageTypeMappings = new Dictionary(); + + Target.MessageTypeMappings[typeof(T).Name] = typeof(T); + return (TBuilder)this; } } diff --git a/src/Foundatio/Metrics/BufferedMetricsClientBase.cs b/src/Foundatio/Metrics/BufferedMetricsClientBase.cs index 906956ba5..f2c7665b1 100644 --- a/src/Foundatio/Metrics/BufferedMetricsClientBase.cs +++ b/src/Foundatio/Metrics/BufferedMetricsClientBase.cs @@ -10,355 +10,354 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -namespace Foundatio.Metrics +namespace Foundatio.Metrics; + +public abstract class BufferedMetricsClientBase : IBufferedMetricsClient { - public abstract class BufferedMetricsClientBase : IBufferedMetricsClient + protected readonly List _timeBuckets = new() { + new TimeBucket { Size = TimeSpan.FromMinutes(1) } + }; + + private readonly ConcurrentQueue _queue = new(); + private readonly Timer _flushTimer; + private readonly SharedMetricsClientOptions _options; + protected readonly ILogger _logger; + + public BufferedMetricsClientBase(SharedMetricsClientOptions options) { - protected readonly List _timeBuckets = new() { - new TimeBucket { Size = TimeSpan.FromMinutes(1) } - }; + _options = options; + _logger = options.LoggerFactory?.CreateLogger(GetType()) ?? NullLogger.Instance; + if (options.Buffered) + _flushTimer = new Timer(OnMetricsTimer, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2)); + } - private readonly ConcurrentQueue _queue = new(); - private readonly Timer _flushTimer; - private readonly SharedMetricsClientOptions _options; - protected readonly ILogger _logger; + public AsyncEvent Counted { get; } = new AsyncEvent(true); - public BufferedMetricsClientBase(SharedMetricsClientOptions options) - { - _options = options; - _logger = options.LoggerFactory?.CreateLogger(GetType()) ?? NullLogger.Instance; - if (options.Buffered) - _flushTimer = new Timer(OnMetricsTimer, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2)); - } + protected virtual Task OnCountedAsync(long value) + { + var counted = Counted; + if (counted == null) + return Task.CompletedTask; - public AsyncEvent Counted { get; } = new AsyncEvent(true); + var args = new CountedEventArgs { Value = value }; + return counted.InvokeAsync(this, args); + } - protected virtual Task OnCountedAsync(long value) - { - var counted = Counted; - if (counted == null) - return Task.CompletedTask; + public void Counter(string name, int value = 1) + { + var entry = new MetricEntry { Name = name, Type = MetricType.Counter, Counter = value }; + if (!_options.Buffered) + SubmitMetric(entry); + else + _queue.Enqueue(entry); + } - var args = new CountedEventArgs { Value = value }; - return counted.InvokeAsync(this, args); - } + public void Gauge(string name, double value) + { + var entry = new MetricEntry { Name = name, Type = MetricType.Gauge, Gauge = value }; + if (!_options.Buffered) + SubmitMetric(entry); + else + _queue.Enqueue(entry); + } - public void Counter(string name, int value = 1) - { - var entry = new MetricEntry { Name = name, Type = MetricType.Counter, Counter = value }; - if (!_options.Buffered) - SubmitMetric(entry); - else - _queue.Enqueue(entry); - } + public void Timer(string name, int milliseconds) + { + var entry = new MetricEntry { Name = name, Type = MetricType.Timing, Timing = milliseconds }; + if (!_options.Buffered) + SubmitMetric(entry); + else + _queue.Enqueue(entry); + } + + private void OnMetricsTimer(object state) + { + if (_sendingMetrics || _queue.IsEmpty) + return; - public void Gauge(string name, double value) + try { - var entry = new MetricEntry { Name = name, Type = MetricType.Gauge, Gauge = value }; - if (!_options.Buffered) - SubmitMetric(entry); - else - _queue.Enqueue(entry); + FlushAsync().AnyContext().GetAwaiter().GetResult(); } - - public void Timer(string name, int milliseconds) + catch (Exception ex) { - var entry = new MetricEntry { Name = name, Type = MetricType.Timing, Timing = milliseconds }; - if (!_options.Buffered) - SubmitMetric(entry); - else - _queue.Enqueue(entry); + if (_logger.IsEnabled(LogLevel.Error)) + _logger.LogError(ex, "Error flushing metrics: {Message}", ex.Message); } + } - private void OnMetricsTimer(object state) - { - if (_sendingMetrics || _queue.IsEmpty) - return; + private bool _sendingMetrics = false; + public async Task FlushAsync() + { + if (_sendingMetrics || _queue.IsEmpty) + return; - try - { - FlushAsync().AnyContext().GetAwaiter().GetResult(); - } - catch (Exception ex) - { - if (_logger.IsEnabled(LogLevel.Error)) - _logger.LogError(ex, "Error flushing metrics: {Message}", ex.Message); - } - } + bool isTraceLogLevelEnabled = _logger.IsEnabled(LogLevel.Trace); + if (isTraceLogLevelEnabled) _logger.LogTrace("Flushing metrics: count={Count}", _queue.Count); - private bool _sendingMetrics = false; - public async Task FlushAsync() + try { - if (_sendingMetrics || _queue.IsEmpty) - return; - - bool isTraceLogLevelEnabled = _logger.IsEnabled(LogLevel.Trace); - if (isTraceLogLevelEnabled) _logger.LogTrace("Flushing metrics: count={Count}", _queue.Count); + _sendingMetrics = true; - try + var startTime = SystemClock.UtcNow; + var entries = new List(); + while (_queue.TryDequeue(out var entry)) { - _sendingMetrics = true; - - var startTime = SystemClock.UtcNow; - var entries = new List(); - while (_queue.TryDequeue(out var entry)) - { - entries.Add(entry); - if (entry.EnqueuedDate > startTime) - break; - } + entries.Add(entry); + if (entry.EnqueuedDate > startTime) + break; + } - if (entries.Count == 0) - return; + if (entries.Count == 0) + return; - if (isTraceLogLevelEnabled) _logger.LogTrace("Dequeued {Count} metrics", entries.Count); - await SubmitMetricsAsync(entries).AnyContext(); - } - finally - { - _sendingMetrics = false; - } + if (isTraceLogLevelEnabled) _logger.LogTrace("Dequeued {Count} metrics", entries.Count); + await SubmitMetricsAsync(entries).AnyContext(); } - - private void SubmitMetric(MetricEntry metric) + finally { - SubmitMetricsAsync(new List { metric }).AnyContext().GetAwaiter().GetResult(); + _sendingMetrics = false; } + } - protected virtual async Task SubmitMetricsAsync(List metrics) - { - bool isTraceLogLevelEnabled = _logger.IsEnabled(LogLevel.Trace); - foreach (var timeBucket in _timeBuckets) - { - try - { - // counters - var counters = metrics.Where(e => e.Type == MetricType.Counter).ToList(); - var groupedCounters = counters - .GroupBy(e => new MetricKey(e.EnqueuedDate.Floor(timeBucket.Size), timeBucket.Size, e.Name)) - .Select(e => new AggregatedCounterMetric - { - Key = e.Key, - Value = e.Sum(c => c.Counter), - Entries = e.ToList() - }).ToList(); - - if (metrics.Count > 1 && counters.Count > 0 && isTraceLogLevelEnabled) - _logger.LogTrace("Aggregated {CountersCount} counter(s) into {GroupedCountersCount} counter group(s)", counters.Count, groupedCounters.Count); - - // gauges - var gauges = metrics.Where(e => e.Type == MetricType.Gauge).ToList(); - var groupedGauges = gauges - .GroupBy(e => new MetricKey(e.EnqueuedDate.Floor(timeBucket.Size), timeBucket.Size, e.Name)) - .Select(e => new AggregatedGaugeMetric - { - Key = e.Key, - Count = e.Count(), - Total = e.Sum(c => c.Gauge), - Last = e.Last().Gauge, - Min = e.Min(c => c.Gauge), - Max = e.Max(c => c.Gauge), - Entries = e.ToList() - }).ToList(); - - if (metrics.Count > 1 && gauges.Count > 0 && isTraceLogLevelEnabled) - _logger.LogTrace("Aggregated {GaugesCount} gauge(s) into {GroupedGaugesCount} gauge group(s)", gauges.Count, groupedGauges.Count); - - // timings - var timings = metrics.Where(e => e.Type == MetricType.Timing).ToList(); - var groupedTimings = timings - .GroupBy(e => new MetricKey(e.EnqueuedDate.Floor(timeBucket.Size), timeBucket.Size, e.Name)) - .Select(e => new AggregatedTimingMetric - { - Key = e.Key, - Count = e.Count(), - TotalDuration = e.Sum(c => (long)c.Timing), - MinDuration = e.Min(c => c.Timing), - MaxDuration = e.Max(c => c.Timing), - Entries = e.ToList() - }).ToList(); - - if (metrics.Count > 1 && timings.Count > 0 && isTraceLogLevelEnabled) - _logger.LogTrace("Aggregated {TimingsCount} timing(s) into {GroupedTimingsCount} timing group(s)", timings.Count, groupedTimings.Count); - - // store aggregated metrics - if (counters.Count > 0 || gauges.Count > 0 || timings.Count > 0) - await StoreAggregatedMetricsInternalAsync(timeBucket, groupedCounters, groupedGauges, groupedTimings).AnyContext(); - } - catch (Exception ex) - { - if (_logger.IsEnabled(LogLevel.Error)) - _logger.LogError(ex, "Error aggregating metrics: {Message}", ex.Message); - throw; - } - } - } + private void SubmitMetric(MetricEntry metric) + { + SubmitMetricsAsync(new List { metric }).AnyContext().GetAwaiter().GetResult(); + } - private async Task StoreAggregatedMetricsInternalAsync(TimeBucket timeBucket, ICollection counters, ICollection gauges, ICollection timings) + protected virtual async Task SubmitMetricsAsync(List metrics) + { + bool isTraceLogLevelEnabled = _logger.IsEnabled(LogLevel.Trace); + foreach (var timeBucket in _timeBuckets) { - bool isTraceLogLevelEnabled = _logger.IsEnabled(LogLevel.Trace); - if (isTraceLogLevelEnabled) - _logger.LogTrace("Storing {CountersCount} counters, {GaugesCount} gauges, {TimingsCount} timings.", counters.Count, gauges.Count, timings.Count); - try { - await Run.WithRetriesAsync(() => StoreAggregatedMetricsAsync(timeBucket, counters, gauges, timings)).AnyContext(); + // counters + var counters = metrics.Where(e => e.Type == MetricType.Counter).ToList(); + var groupedCounters = counters + .GroupBy(e => new MetricKey(e.EnqueuedDate.Floor(timeBucket.Size), timeBucket.Size, e.Name)) + .Select(e => new AggregatedCounterMetric + { + Key = e.Key, + Value = e.Sum(c => c.Counter), + Entries = e.ToList() + }).ToList(); + + if (metrics.Count > 1 && counters.Count > 0 && isTraceLogLevelEnabled) + _logger.LogTrace("Aggregated {CountersCount} counter(s) into {GroupedCountersCount} counter group(s)", counters.Count, groupedCounters.Count); + + // gauges + var gauges = metrics.Where(e => e.Type == MetricType.Gauge).ToList(); + var groupedGauges = gauges + .GroupBy(e => new MetricKey(e.EnqueuedDate.Floor(timeBucket.Size), timeBucket.Size, e.Name)) + .Select(e => new AggregatedGaugeMetric + { + Key = e.Key, + Count = e.Count(), + Total = e.Sum(c => c.Gauge), + Last = e.Last().Gauge, + Min = e.Min(c => c.Gauge), + Max = e.Max(c => c.Gauge), + Entries = e.ToList() + }).ToList(); + + if (metrics.Count > 1 && gauges.Count > 0 && isTraceLogLevelEnabled) + _logger.LogTrace("Aggregated {GaugesCount} gauge(s) into {GroupedGaugesCount} gauge group(s)", gauges.Count, groupedGauges.Count); + + // timings + var timings = metrics.Where(e => e.Type == MetricType.Timing).ToList(); + var groupedTimings = timings + .GroupBy(e => new MetricKey(e.EnqueuedDate.Floor(timeBucket.Size), timeBucket.Size, e.Name)) + .Select(e => new AggregatedTimingMetric + { + Key = e.Key, + Count = e.Count(), + TotalDuration = e.Sum(c => (long)c.Timing), + MinDuration = e.Min(c => c.Timing), + MaxDuration = e.Max(c => c.Timing), + Entries = e.ToList() + }).ToList(); + + if (metrics.Count > 1 && timings.Count > 0 && isTraceLogLevelEnabled) + _logger.LogTrace("Aggregated {TimingsCount} timing(s) into {GroupedTimingsCount} timing group(s)", timings.Count, groupedTimings.Count); + + // store aggregated metrics + if (counters.Count > 0 || gauges.Count > 0 || timings.Count > 0) + await StoreAggregatedMetricsInternalAsync(timeBucket, groupedCounters, groupedGauges, groupedTimings).AnyContext(); } catch (Exception ex) { if (_logger.IsEnabled(LogLevel.Error)) - _logger.LogError(ex, "Error storing aggregated metrics: {Message}", ex.Message); + _logger.LogError(ex, "Error aggregating metrics: {Message}", ex.Message); throw; } - - await OnCountedAsync(counters.Sum(c => c.Value)).AnyContext(); - if (isTraceLogLevelEnabled) _logger.LogTrace("Done storing aggregated metrics"); } + } - protected abstract Task StoreAggregatedMetricsAsync(TimeBucket timeBucket, ICollection counters, ICollection gauges, ICollection timings); + private async Task StoreAggregatedMetricsInternalAsync(TimeBucket timeBucket, ICollection counters, ICollection gauges, ICollection timings) + { + bool isTraceLogLevelEnabled = _logger.IsEnabled(LogLevel.Trace); + if (isTraceLogLevelEnabled) + _logger.LogTrace("Storing {CountersCount} counters, {GaugesCount} gauges, {TimingsCount} timings.", counters.Count, gauges.Count, timings.Count); - public async Task WaitForCounterAsync(string statName, long count = 1, TimeSpan? timeout = null) + try { - using var cancellationTokenSource = timeout.ToCancellationTokenSource(TimeSpan.FromSeconds(10)); - return await WaitForCounterAsync(statName, () => Task.CompletedTask, count, cancellationTokenSource.Token).AnyContext(); + await Run.WithRetriesAsync(() => StoreAggregatedMetricsAsync(timeBucket, counters, gauges, timings)).AnyContext(); } - - public async Task WaitForCounterAsync(string statName, Func work, long count = 1, CancellationToken cancellationToken = default) + catch (Exception ex) { - if (count <= 0) - return true; + if (_logger.IsEnabled(LogLevel.Error)) + _logger.LogError(ex, "Error storing aggregated metrics: {Message}", ex.Message); + throw; + } - long currentCount = count; - var resetEvent = new AsyncAutoResetEvent(false); - var start = SystemClock.UtcNow; + await OnCountedAsync(counters.Sum(c => c.Value)).AnyContext(); + if (isTraceLogLevelEnabled) _logger.LogTrace("Done storing aggregated metrics"); + } - bool isTraceLogLevelEnabled = _logger.IsEnabled(LogLevel.Trace); - using (Counted.AddHandler((s, e) => - { - currentCount -= e.Value; - resetEvent.Set(); - return Task.CompletedTask; - })) - { - if (isTraceLogLevelEnabled) _logger.LogTrace("Wait: count={Count}", currentCount); + protected abstract Task StoreAggregatedMetricsAsync(TimeBucket timeBucket, ICollection counters, ICollection gauges, ICollection timings); - if (work != null) - await work().AnyContext(); + public async Task WaitForCounterAsync(string statName, long count = 1, TimeSpan? timeout = null) + { + using var cancellationTokenSource = timeout.ToCancellationTokenSource(TimeSpan.FromSeconds(10)); + return await WaitForCounterAsync(statName, () => Task.CompletedTask, count, cancellationTokenSource.Token).AnyContext(); + } - if (currentCount <= 0) - return true; + public async Task WaitForCounterAsync(string statName, Func work, long count = 1, CancellationToken cancellationToken = default) + { + if (count <= 0) + return true; + + long currentCount = count; + var resetEvent = new AsyncAutoResetEvent(false); + var start = SystemClock.UtcNow; + + bool isTraceLogLevelEnabled = _logger.IsEnabled(LogLevel.Trace); + using (Counted.AddHandler((s, e) => + { + currentCount -= e.Value; + resetEvent.Set(); + return Task.CompletedTask; + })) + { + if (isTraceLogLevelEnabled) _logger.LogTrace("Wait: count={Count}", currentCount); - do - { - try - { - await resetEvent.WaitAsync(cancellationToken).AnyContext(); - } - catch (OperationCanceledException) { } + if (work != null) + await work().AnyContext(); - if (isTraceLogLevelEnabled) - _logger.LogTrace("Got signal: count={CurrentCount} expected={Count}", currentCount, count); - } while (cancellationToken.IsCancellationRequested == false && currentCount > 0); - } + if (currentCount <= 0) + return true; - if (isTraceLogLevelEnabled) - _logger.LogTrace("Done waiting: count={CurrentCount} expected={Count} success={Success} time={Time}", currentCount, count, currentCount <= 0, SystemClock.UtcNow.Subtract(start)); + do + { + try + { + await resetEvent.WaitAsync(cancellationToken).AnyContext(); + } + catch (OperationCanceledException) { } - return currentCount <= 0; + if (isTraceLogLevelEnabled) + _logger.LogTrace("Got signal: count={CurrentCount} expected={Count}", currentCount, count); + } while (cancellationToken.IsCancellationRequested == false && currentCount > 0); } - public virtual void Dispose() - { - _flushTimer?.Dispose(); - FlushAsync().AnyContext().GetAwaiter().GetResult(); - _queue?.Clear(); - } + if (isTraceLogLevelEnabled) + _logger.LogTrace("Done waiting: count={CurrentCount} expected={Count} success={Success} time={Time}", currentCount, count, currentCount <= 0, SystemClock.UtcNow.Subtract(start)); - [DebuggerDisplay("Date: {EnqueuedDate} Type: {Type} Name: {Name} Counter: {Counter} Gauge: {Gauge} Timing: {Timing}")] - protected class MetricEntry - { - public DateTime EnqueuedDate { get; } = SystemClock.UtcNow; - public string Name { get; set; } - public MetricType Type { get; set; } - public int Counter { get; set; } - public double Gauge { get; set; } - public int Timing { get; set; } - } + return currentCount <= 0; + } - protected enum MetricType - { - Counter, - Gauge, - Timing - } + public virtual void Dispose() + { + _flushTimer?.Dispose(); + FlushAsync().AnyContext().GetAwaiter().GetResult(); + _queue?.Clear(); + } - [DebuggerDisplay("Time: {Time} Key: {Key}")] - protected class MetricBucket - { - public string Key { get; set; } - public DateTime Time { get; set; } - } + [DebuggerDisplay("Date: {EnqueuedDate} Type: {Type} Name: {Name} Counter: {Counter} Gauge: {Gauge} Timing: {Timing}")] + protected class MetricEntry + { + public DateTime EnqueuedDate { get; } = SystemClock.UtcNow; + public string Name { get; set; } + public MetricType Type { get; set; } + public int Counter { get; set; } + public double Gauge { get; set; } + public int Timing { get; set; } + } - protected interface IAggregatedMetric where T : class - { - MetricKey Key { get; set; } - ICollection Entries { get; set; } - T Add(T other); - } + protected enum MetricType + { + Counter, + Gauge, + Timing + } - protected class AggregatedCounterMetric : IAggregatedMetric - { - public MetricKey Key { get; set; } - public long Value { get; set; } - public ICollection Entries { get; set; } + [DebuggerDisplay("Time: {Time} Key: {Key}")] + protected class MetricBucket + { + public string Key { get; set; } + public DateTime Time { get; set; } + } - public AggregatedCounterMetric Add(AggregatedCounterMetric other) - { - return this; - } - } + protected interface IAggregatedMetric where T : class + { + MetricKey Key { get; set; } + ICollection Entries { get; set; } + T Add(T other); + } - protected class AggregatedGaugeMetric : IAggregatedMetric + protected class AggregatedCounterMetric : IAggregatedMetric + { + public MetricKey Key { get; set; } + public long Value { get; set; } + public ICollection Entries { get; set; } + + public AggregatedCounterMetric Add(AggregatedCounterMetric other) { - public MetricKey Key { get; set; } - public int Count { get; set; } - public double Total { get; set; } - public double Last { get; set; } - public double Min { get; set; } - public double Max { get; set; } - public ICollection Entries { get; set; } - - public AggregatedGaugeMetric Add(AggregatedGaugeMetric other) - { - return this; - } + return this; } + } - protected class AggregatedTimingMetric : IAggregatedMetric + protected class AggregatedGaugeMetric : IAggregatedMetric + { + public MetricKey Key { get; set; } + public int Count { get; set; } + public double Total { get; set; } + public double Last { get; set; } + public double Min { get; set; } + public double Max { get; set; } + public ICollection Entries { get; set; } + + public AggregatedGaugeMetric Add(AggregatedGaugeMetric other) { - public MetricKey Key { get; set; } - public int Count { get; set; } - public long TotalDuration { get; set; } - public int MinDuration { get; set; } - public int MaxDuration { get; set; } - public ICollection Entries { get; set; } - - public AggregatedTimingMetric Add(AggregatedTimingMetric other) - { - return this; - } + return this; } + } - [DebuggerDisplay("Size: {Size} Ttl: {Ttl}")] - protected struct TimeBucket + protected class AggregatedTimingMetric : IAggregatedMetric + { + public MetricKey Key { get; set; } + public int Count { get; set; } + public long TotalDuration { get; set; } + public int MinDuration { get; set; } + public int MaxDuration { get; set; } + public ICollection Entries { get; set; } + + public AggregatedTimingMetric Add(AggregatedTimingMetric other) { - public TimeSpan Size { get; set; } - public TimeSpan Ttl { get; set; } + return this; } } - public class CountedEventArgs : EventArgs + [DebuggerDisplay("Size: {Size} Ttl: {Ttl}")] + protected struct TimeBucket { - public long Value { get; set; } + public TimeSpan Size { get; set; } + public TimeSpan Ttl { get; set; } } } + +public class CountedEventArgs : EventArgs +{ + public long Value { get; set; } +} diff --git a/src/Foundatio/Metrics/CacheBucketMetricsClientBase.cs b/src/Foundatio/Metrics/CacheBucketMetricsClientBase.cs index 6512a54cb..366e14f7d 100644 --- a/src/Foundatio/Metrics/CacheBucketMetricsClientBase.cs +++ b/src/Foundatio/Metrics/CacheBucketMetricsClientBase.cs @@ -6,278 +6,277 @@ using Foundatio.Utility; using Microsoft.Extensions.Logging; -namespace Foundatio.Metrics +namespace Foundatio.Metrics; + +public abstract class CacheBucketMetricsClientBase : BufferedMetricsClientBase, IMetricsClientStats { - public abstract class CacheBucketMetricsClientBase : BufferedMetricsClientBase, IMetricsClientStats + protected readonly ICacheClient _cache; + private readonly string _prefix; + + public CacheBucketMetricsClientBase(ICacheClient cache, SharedMetricsClientOptions options) : base(options) { - protected readonly ICacheClient _cache; - private readonly string _prefix; + _cache = cache; + _prefix = !String.IsNullOrEmpty(options.Prefix) ? (!options.Prefix.EndsWith(":") ? options.Prefix + ":" : options.Prefix) : String.Empty; - public CacheBucketMetricsClientBase(ICacheClient cache, SharedMetricsClientOptions options) : base(options) - { - _cache = cache; - _prefix = !String.IsNullOrEmpty(options.Prefix) ? (!options.Prefix.EndsWith(":") ? options.Prefix + ":" : options.Prefix) : String.Empty; + _timeBuckets.Clear(); + _timeBuckets.Add(new TimeBucket { Size = TimeSpan.FromMinutes(5), Ttl = TimeSpan.FromHours(1) }); + _timeBuckets.Add(new TimeBucket { Size = TimeSpan.FromHours(1), Ttl = TimeSpan.FromDays(7) }); + } - _timeBuckets.Clear(); - _timeBuckets.Add(new TimeBucket { Size = TimeSpan.FromMinutes(5), Ttl = TimeSpan.FromHours(1) }); - _timeBuckets.Add(new TimeBucket { Size = TimeSpan.FromHours(1), Ttl = TimeSpan.FromDays(7) }); - } + protected override Task StoreAggregatedMetricsAsync(TimeBucket timeBucket, ICollection counters, ICollection gauges, ICollection timings) + { + var tasks = new List(); + foreach (var counter in counters) + tasks.Add(StoreCounterAsync(timeBucket, counter)); - protected override Task StoreAggregatedMetricsAsync(TimeBucket timeBucket, ICollection counters, ICollection gauges, ICollection timings) - { - var tasks = new List(); - foreach (var counter in counters) - tasks.Add(StoreCounterAsync(timeBucket, counter)); + foreach (var gauge in gauges) + tasks.Add(StoreGaugeAsync(timeBucket, gauge)); - foreach (var gauge in gauges) - tasks.Add(StoreGaugeAsync(timeBucket, gauge)); + foreach (var timing in timings) + tasks.Add(StoreTimingAsync(timeBucket, timing)); - foreach (var timing in timings) - tasks.Add(StoreTimingAsync(timeBucket, timing)); + return Task.WhenAll(tasks); + } - return Task.WhenAll(tasks); - } + private async Task StoreCounterAsync(TimeBucket timeBucket, AggregatedCounterMetric counter) + { + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.LogTrace("Storing counter name={Name} value={Value} time={Duration}", counter.Key.Name, counter.Value, counter.Key.Duration); - private async Task StoreCounterAsync(TimeBucket timeBucket, AggregatedCounterMetric counter) - { - if (_logger.IsEnabled(LogLevel.Trace)) - _logger.LogTrace("Storing counter name={Name} value={Value} time={Duration}", counter.Key.Name, counter.Value, counter.Key.Duration); + string bucketKey = GetBucketKey(CacheMetricNames.Counter, counter.Key.Name, counter.Key.StartTimeUtc, timeBucket.Size); + await _cache.IncrementAsync(bucketKey, counter.Value, timeBucket.Ttl).AnyContext(); - string bucketKey = GetBucketKey(CacheMetricNames.Counter, counter.Key.Name, counter.Key.StartTimeUtc, timeBucket.Size); - await _cache.IncrementAsync(bucketKey, counter.Value, timeBucket.Ttl).AnyContext(); + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Done storing counter name={Name}", counter.Key.Name); + } - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Done storing counter name={Name}", counter.Key.Name); - } + private async Task StoreGaugeAsync(TimeBucket timeBucket, AggregatedGaugeMetric gauge) + { + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.LogTrace("Storing gauge name={Name} count={Count} total={Total} last={Last} min={Min} max={Max} time={StartTimeUtc}", gauge.Key.Name, gauge.Count, gauge.Total, gauge.Last, gauge.Min, gauge.Max, gauge.Key.StartTimeUtc); + + string countKey = GetBucketKey(CacheMetricNames.Gauge, gauge.Key.Name, gauge.Key.StartTimeUtc, timeBucket.Size, CacheMetricNames.Count); + string totalDurationKey = GetBucketKey(CacheMetricNames.Gauge, gauge.Key.Name, gauge.Key.StartTimeUtc, timeBucket.Size, CacheMetricNames.Total); + string lastKey = GetBucketKey(CacheMetricNames.Gauge, gauge.Key.Name, gauge.Key.StartTimeUtc, timeBucket.Size, CacheMetricNames.Last); + string minKey = GetBucketKey(CacheMetricNames.Gauge, gauge.Key.Name, gauge.Key.StartTimeUtc, timeBucket.Size, CacheMetricNames.Min); + string maxKey = GetBucketKey(CacheMetricNames.Gauge, gauge.Key.Name, gauge.Key.StartTimeUtc, timeBucket.Size, CacheMetricNames.Max); + + await Task.WhenAll( + _cache.IncrementAsync(countKey, gauge.Count, timeBucket.Ttl), + _cache.IncrementAsync(totalDurationKey, gauge.Total, timeBucket.Ttl), + _cache.SetAsync(lastKey, gauge.Last, timeBucket.Ttl), + _cache.SetIfLowerAsync(minKey, gauge.Min, timeBucket.Ttl), + _cache.SetIfHigherAsync(maxKey, gauge.Max, timeBucket.Ttl) + ).AnyContext(); + + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Done storing gauge name={Name}", gauge.Key.Name); + } - private async Task StoreGaugeAsync(TimeBucket timeBucket, AggregatedGaugeMetric gauge) - { - if (_logger.IsEnabled(LogLevel.Trace)) - _logger.LogTrace("Storing gauge name={Name} count={Count} total={Total} last={Last} min={Min} max={Max} time={StartTimeUtc}", gauge.Key.Name, gauge.Count, gauge.Total, gauge.Last, gauge.Min, gauge.Max, gauge.Key.StartTimeUtc); - - string countKey = GetBucketKey(CacheMetricNames.Gauge, gauge.Key.Name, gauge.Key.StartTimeUtc, timeBucket.Size, CacheMetricNames.Count); - string totalDurationKey = GetBucketKey(CacheMetricNames.Gauge, gauge.Key.Name, gauge.Key.StartTimeUtc, timeBucket.Size, CacheMetricNames.Total); - string lastKey = GetBucketKey(CacheMetricNames.Gauge, gauge.Key.Name, gauge.Key.StartTimeUtc, timeBucket.Size, CacheMetricNames.Last); - string minKey = GetBucketKey(CacheMetricNames.Gauge, gauge.Key.Name, gauge.Key.StartTimeUtc, timeBucket.Size, CacheMetricNames.Min); - string maxKey = GetBucketKey(CacheMetricNames.Gauge, gauge.Key.Name, gauge.Key.StartTimeUtc, timeBucket.Size, CacheMetricNames.Max); - - await Task.WhenAll( - _cache.IncrementAsync(countKey, gauge.Count, timeBucket.Ttl), - _cache.IncrementAsync(totalDurationKey, gauge.Total, timeBucket.Ttl), - _cache.SetAsync(lastKey, gauge.Last, timeBucket.Ttl), - _cache.SetIfLowerAsync(minKey, gauge.Min, timeBucket.Ttl), - _cache.SetIfHigherAsync(maxKey, gauge.Max, timeBucket.Ttl) - ).AnyContext(); - - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Done storing gauge name={Name}", gauge.Key.Name); - } + private async Task StoreTimingAsync(TimeBucket timeBucket, AggregatedTimingMetric timing) + { + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.LogTrace("Storing timing name={Name} count={Count} total={TotalDuration} min={MinDuration} max={MaxDuration} time={StartTimeUtc}", timing.Key.Name, timing.Count, timing.TotalDuration, timing.MinDuration, timing.MaxDuration, timing.Key.StartTimeUtc); + + string countKey = GetBucketKey(CacheMetricNames.Timing, timing.Key.Name, timing.Key.StartTimeUtc, timeBucket.Size, CacheMetricNames.Count); + string totalDurationKey = GetBucketKey(CacheMetricNames.Timing, timing.Key.Name, timing.Key.StartTimeUtc, timeBucket.Size, CacheMetricNames.Total); + string maxKey = GetBucketKey(CacheMetricNames.Timing, timing.Key.Name, timing.Key.StartTimeUtc, timeBucket.Size, CacheMetricNames.Max); + string minKey = GetBucketKey(CacheMetricNames.Timing, timing.Key.Name, timing.Key.StartTimeUtc, timeBucket.Size, CacheMetricNames.Min); + + await Task.WhenAll( + _cache.IncrementAsync(countKey, timing.Count, timeBucket.Ttl), + _cache.IncrementAsync(totalDurationKey, timing.TotalDuration, timeBucket.Ttl), + _cache.SetIfHigherAsync(maxKey, timing.MaxDuration, timeBucket.Ttl), + _cache.SetIfLowerAsync(minKey, timing.MinDuration, timeBucket.Ttl) + ).AnyContext(); + + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Done storing timing name={Name}", timing.Key.Name); + } - private async Task StoreTimingAsync(TimeBucket timeBucket, AggregatedTimingMetric timing) - { - if (_logger.IsEnabled(LogLevel.Trace)) - _logger.LogTrace("Storing timing name={Name} count={Count} total={TotalDuration} min={MinDuration} max={MaxDuration} time={StartTimeUtc}", timing.Key.Name, timing.Count, timing.TotalDuration, timing.MinDuration, timing.MaxDuration, timing.Key.StartTimeUtc); - - string countKey = GetBucketKey(CacheMetricNames.Timing, timing.Key.Name, timing.Key.StartTimeUtc, timeBucket.Size, CacheMetricNames.Count); - string totalDurationKey = GetBucketKey(CacheMetricNames.Timing, timing.Key.Name, timing.Key.StartTimeUtc, timeBucket.Size, CacheMetricNames.Total); - string maxKey = GetBucketKey(CacheMetricNames.Timing, timing.Key.Name, timing.Key.StartTimeUtc, timeBucket.Size, CacheMetricNames.Max); - string minKey = GetBucketKey(CacheMetricNames.Timing, timing.Key.Name, timing.Key.StartTimeUtc, timeBucket.Size, CacheMetricNames.Min); - - await Task.WhenAll( - _cache.IncrementAsync(countKey, timing.Count, timeBucket.Ttl), - _cache.IncrementAsync(totalDurationKey, timing.TotalDuration, timeBucket.Ttl), - _cache.SetIfHigherAsync(maxKey, timing.MaxDuration, timeBucket.Ttl), - _cache.SetIfLowerAsync(minKey, timing.MinDuration, timeBucket.Ttl) - ).AnyContext(); - - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Done storing timing name={Name}", timing.Key.Name); - } + public async Task GetCounterStatsAsync(string name, DateTime? start = null, DateTime? end = null, int dataPoints = 20) + { + if (!start.HasValue) + start = SystemClock.UtcNow.AddHours(-4); - public async Task GetCounterStatsAsync(string name, DateTime? start = null, DateTime? end = null, int dataPoints = 20) - { - if (!start.HasValue) - start = SystemClock.UtcNow.AddHours(-4); + if (!end.HasValue) + end = SystemClock.UtcNow; - if (!end.HasValue) - end = SystemClock.UtcNow; + var interval = end.Value.Subtract(start.Value).TotalMinutes > 180 ? TimeSpan.FromHours(1) : TimeSpan.FromMinutes(5); - var interval = end.Value.Subtract(start.Value).TotalMinutes > 180 ? TimeSpan.FromHours(1) : TimeSpan.FromMinutes(5); + var countBuckets = GetMetricBuckets(CacheMetricNames.Counter, name, start.Value, end.Value, interval); + var countResults = await _cache.GetAllAsync(countBuckets.Select(k => k.Key)).AnyContext(); - var countBuckets = GetMetricBuckets(CacheMetricNames.Counter, name, start.Value, end.Value, interval); - var countResults = await _cache.GetAllAsync(countBuckets.Select(k => k.Key)).AnyContext(); + ICollection stats = new List(); + foreach (var bucket in countBuckets) + { + string countKey = bucket.Key; - ICollection stats = new List(); - foreach (var bucket in countBuckets) + stats.Add(new CounterStat { - string countKey = bucket.Key; + Time = bucket.Time, + Count = countResults[countKey].Value + }); + } - stats.Add(new CounterStat - { - Time = bucket.Time, - Count = countResults[countKey].Value - }); - } + stats = stats.ReduceTimeSeries(s => s.Time, (s, d) => new CounterStat + { + Time = d, + Count = s.Sum(i => i.Count) + }, dataPoints); - stats = stats.ReduceTimeSeries(s => s.Time, (s, d) => new CounterStat - { - Time = d, - Count = s.Sum(i => i.Count) - }, dataPoints); + return new CounterStatSummary(name, stats, start.Value, end.Value); + } - return new CounterStatSummary(name, stats, start.Value, end.Value); - } + public async Task GetGaugeStatsAsync(string name, DateTime? start = null, DateTime? end = null, int dataPoints = 20) + { + if (!start.HasValue) + start = SystemClock.UtcNow.AddHours(-4); - public async Task GetGaugeStatsAsync(string name, DateTime? start = null, DateTime? end = null, int dataPoints = 20) - { - if (!start.HasValue) - start = SystemClock.UtcNow.AddHours(-4); + if (!end.HasValue) + end = SystemClock.UtcNow; - if (!end.HasValue) - end = SystemClock.UtcNow; + var interval = end.Value.Subtract(start.Value).TotalMinutes > 180 ? TimeSpan.FromHours(1) : TimeSpan.FromMinutes(5); - var interval = end.Value.Subtract(start.Value).TotalMinutes > 180 ? TimeSpan.FromHours(1) : TimeSpan.FromMinutes(5); + var countBuckets = GetMetricBuckets(CacheMetricNames.Gauge, name, start.Value, end.Value, interval, CacheMetricNames.Count); + var totalBuckets = GetMetricBuckets(CacheMetricNames.Gauge, name, start.Value, end.Value, interval, CacheMetricNames.Total); + var lastBuckets = GetMetricBuckets(CacheMetricNames.Gauge, name, start.Value, end.Value, interval, CacheMetricNames.Last); + var minBuckets = GetMetricBuckets(CacheMetricNames.Gauge, name, start.Value, end.Value, interval, CacheMetricNames.Min); + var maxBuckets = GetMetricBuckets(CacheMetricNames.Gauge, name, start.Value, end.Value, interval, CacheMetricNames.Max); - var countBuckets = GetMetricBuckets(CacheMetricNames.Gauge, name, start.Value, end.Value, interval, CacheMetricNames.Count); - var totalBuckets = GetMetricBuckets(CacheMetricNames.Gauge, name, start.Value, end.Value, interval, CacheMetricNames.Total); - var lastBuckets = GetMetricBuckets(CacheMetricNames.Gauge, name, start.Value, end.Value, interval, CacheMetricNames.Last); - var minBuckets = GetMetricBuckets(CacheMetricNames.Gauge, name, start.Value, end.Value, interval, CacheMetricNames.Min); - var maxBuckets = GetMetricBuckets(CacheMetricNames.Gauge, name, start.Value, end.Value, interval, CacheMetricNames.Max); + var countTask = _cache.GetAllAsync(countBuckets.Select(k => k.Key)); + var totalTask = _cache.GetAllAsync(totalBuckets.Select(k => k.Key)); + var lastTask = _cache.GetAllAsync(lastBuckets.Select(k => k.Key)); + var minTask = _cache.GetAllAsync(minBuckets.Select(k => k.Key)); + var maxTask = _cache.GetAllAsync(maxBuckets.Select(k => k.Key)); - var countTask = _cache.GetAllAsync(countBuckets.Select(k => k.Key)); - var totalTask = _cache.GetAllAsync(totalBuckets.Select(k => k.Key)); - var lastTask = _cache.GetAllAsync(lastBuckets.Select(k => k.Key)); - var minTask = _cache.GetAllAsync(minBuckets.Select(k => k.Key)); - var maxTask = _cache.GetAllAsync(maxBuckets.Select(k => k.Key)); + await Task.WhenAll(countTask, totalTask, lastTask, minTask, maxTask).AnyContext(); - await Task.WhenAll(countTask, totalTask, lastTask, minTask, maxTask).AnyContext(); + ICollection stats = new List(); + for (int i = 0; i < maxBuckets.Count; i++) + { + string countKey = countBuckets[i].Key; + string totalKey = totalBuckets[i].Key; + string minKey = minBuckets[i].Key; + string maxKey = maxBuckets[i].Key; + string lastKey = lastBuckets[i].Key; - ICollection stats = new List(); - for (int i = 0; i < maxBuckets.Count; i++) - { - string countKey = countBuckets[i].Key; - string totalKey = totalBuckets[i].Key; - string minKey = minBuckets[i].Key; - string maxKey = maxBuckets[i].Key; - string lastKey = lastBuckets[i].Key; - - stats.Add(new GaugeStat - { - Time = maxBuckets[i].Time, - Count = countTask.Result[countKey].Value, - Total = totalTask.Result[totalKey].Value, - Min = minTask.Result[minKey].Value, - Max = maxTask.Result[maxKey].Value, - Last = lastTask.Result[lastKey].Value - }); - } - - stats = stats.ReduceTimeSeries(s => s.Time, (s, d) => new GaugeStat + stats.Add(new GaugeStat { - Time = d, - Count = s.Sum(i => i.Count), - Total = s.Sum(i => i.Total), - Min = s.Min(i => i.Min), - Max = s.Max(i => i.Max), - Last = s.Last().Last - }, dataPoints); - - return new GaugeStatSummary(name, stats, start.Value, end.Value); + Time = maxBuckets[i].Time, + Count = countTask.Result[countKey].Value, + Total = totalTask.Result[totalKey].Value, + Min = minTask.Result[minKey].Value, + Max = maxTask.Result[maxKey].Value, + Last = lastTask.Result[lastKey].Value + }); } - public async Task GetTimerStatsAsync(string name, DateTime? start = null, DateTime? end = null, int dataPoints = 20) + stats = stats.ReduceTimeSeries(s => s.Time, (s, d) => new GaugeStat { - if (!start.HasValue) - start = SystemClock.UtcNow.AddHours(-4); + Time = d, + Count = s.Sum(i => i.Count), + Total = s.Sum(i => i.Total), + Min = s.Min(i => i.Min), + Max = s.Max(i => i.Max), + Last = s.Last().Last + }, dataPoints); + + return new GaugeStatSummary(name, stats, start.Value, end.Value); + } - if (!end.HasValue) - end = SystemClock.UtcNow; + public async Task GetTimerStatsAsync(string name, DateTime? start = null, DateTime? end = null, int dataPoints = 20) + { + if (!start.HasValue) + start = SystemClock.UtcNow.AddHours(-4); - var interval = end.Value.Subtract(start.Value).TotalMinutes > 180 ? TimeSpan.FromHours(1) : TimeSpan.FromMinutes(5); + if (!end.HasValue) + end = SystemClock.UtcNow; - var countBuckets = GetMetricBuckets(CacheMetricNames.Timing, name, start.Value, end.Value, interval, CacheMetricNames.Count); - var durationBuckets = GetMetricBuckets(CacheMetricNames.Timing, name, start.Value, end.Value, interval, CacheMetricNames.Total); - var minBuckets = GetMetricBuckets(CacheMetricNames.Timing, name, start.Value, end.Value, interval, CacheMetricNames.Min); - var maxBuckets = GetMetricBuckets(CacheMetricNames.Timing, name, start.Value, end.Value, interval, CacheMetricNames.Max); + var interval = end.Value.Subtract(start.Value).TotalMinutes > 180 ? TimeSpan.FromHours(1) : TimeSpan.FromMinutes(5); - var countTask = _cache.GetAllAsync(countBuckets.Select(k => k.Key)); - var durationTask = _cache.GetAllAsync(durationBuckets.Select(k => k.Key)); - var minTask = _cache.GetAllAsync(minBuckets.Select(k => k.Key)); - var maxTask = _cache.GetAllAsync(maxBuckets.Select(k => k.Key)); + var countBuckets = GetMetricBuckets(CacheMetricNames.Timing, name, start.Value, end.Value, interval, CacheMetricNames.Count); + var durationBuckets = GetMetricBuckets(CacheMetricNames.Timing, name, start.Value, end.Value, interval, CacheMetricNames.Total); + var minBuckets = GetMetricBuckets(CacheMetricNames.Timing, name, start.Value, end.Value, interval, CacheMetricNames.Min); + var maxBuckets = GetMetricBuckets(CacheMetricNames.Timing, name, start.Value, end.Value, interval, CacheMetricNames.Max); - await Task.WhenAll(countTask, durationTask, minTask, maxTask).AnyContext(); + var countTask = _cache.GetAllAsync(countBuckets.Select(k => k.Key)); + var durationTask = _cache.GetAllAsync(durationBuckets.Select(k => k.Key)); + var minTask = _cache.GetAllAsync(minBuckets.Select(k => k.Key)); + var maxTask = _cache.GetAllAsync(maxBuckets.Select(k => k.Key)); - ICollection stats = new List(); - for (int i = 0; i < countBuckets.Count; i++) - { - string countKey = countBuckets[i].Key; - string durationKey = durationBuckets[i].Key; - string minKey = minBuckets[i].Key; - string maxKey = maxBuckets[i].Key; - - stats.Add(new TimingStat - { - Time = countBuckets[i].Time, - Count = countTask.Result[countKey].Value, - TotalDuration = durationTask.Result[durationKey].Value, - MinDuration = minTask.Result[minKey].Value, - MaxDuration = maxTask.Result[maxKey].Value - }); - } - - stats = stats.ReduceTimeSeries(s => s.Time, (s, d) => new TimingStat + await Task.WhenAll(countTask, durationTask, minTask, maxTask).AnyContext(); + + ICollection stats = new List(); + for (int i = 0; i < countBuckets.Count; i++) + { + string countKey = countBuckets[i].Key; + string durationKey = durationBuckets[i].Key; + string minKey = minBuckets[i].Key; + string maxKey = maxBuckets[i].Key; + + stats.Add(new TimingStat { - Time = d, - Count = s.Sum(i => i.Count), - MinDuration = s.Min(i => i.MinDuration), - MaxDuration = s.Max(i => i.MaxDuration), - TotalDuration = s.Sum(i => i.TotalDuration) - }, dataPoints); - - return new TimingStatSummary(name, stats, start.Value, end.Value); + Time = countBuckets[i].Time, + Count = countTask.Result[countKey].Value, + TotalDuration = durationTask.Result[durationKey].Value, + MinDuration = minTask.Result[minKey].Value, + MaxDuration = maxTask.Result[maxKey].Value + }); } - private string GetBucketKey(string metricType, string statName, DateTime? dateTime = null, TimeSpan? interval = null, string suffix = null) + stats = stats.ReduceTimeSeries(s => s.Time, (s, d) => new TimingStat { - if (interval == null) - interval = _timeBuckets[0].Size; + Time = d, + Count = s.Sum(i => i.Count), + MinDuration = s.Min(i => i.MinDuration), + MaxDuration = s.Max(i => i.MaxDuration), + TotalDuration = s.Sum(i => i.TotalDuration) + }, dataPoints); + + return new TimingStatSummary(name, stats, start.Value, end.Value); + } - if (dateTime == null) - dateTime = SystemClock.UtcNow; + private string GetBucketKey(string metricType, string statName, DateTime? dateTime = null, TimeSpan? interval = null, string suffix = null) + { + if (interval == null) + interval = _timeBuckets[0].Size; - dateTime = dateTime.Value.Floor(interval.Value); + if (dateTime == null) + dateTime = SystemClock.UtcNow; - suffix = !String.IsNullOrEmpty(suffix) ? ":" + suffix : String.Empty; - return String.Concat(_prefix, "m:", metricType, ":", statName, ":", interval.Value.TotalMinutes, ":", dateTime.Value.ToString("yy-MM-dd-hh-mm"), suffix); - } + dateTime = dateTime.Value.Floor(interval.Value); - private List GetMetricBuckets(string metricType, string statName, DateTime start, DateTime end, TimeSpan? interval = null, string suffix = null) - { - if (interval == null) - interval = _timeBuckets[0].Size; - - start = start.Floor(interval.Value); - end = end.Floor(interval.Value); + suffix = !String.IsNullOrEmpty(suffix) ? ":" + suffix : String.Empty; + return String.Concat(_prefix, "m:", metricType, ":", statName, ":", interval.Value.TotalMinutes, ":", dateTime.Value.ToString("yy-MM-dd-hh-mm"), suffix); + } - var current = start; - var keys = new List(); - while (current <= end) - { - keys.Add(new MetricBucket { Key = GetBucketKey(metricType, statName, current, interval, suffix), Time = current }); - current = current.Add(interval.Value); - } + private List GetMetricBuckets(string metricType, string statName, DateTime start, DateTime end, TimeSpan? interval = null, string suffix = null) + { + if (interval == null) + interval = _timeBuckets[0].Size; - return keys; - } + start = start.Floor(interval.Value); + end = end.Floor(interval.Value); - private class CacheMetricNames + var current = start; + var keys = new List(); + while (current <= end) { - public const string Counter = "c"; - public const string Gauge = "g"; - public const string Timing = "t"; - - public const string Count = "cnt"; - public const string Total = "tot"; - public const string Max = "max"; - public const string Min = "min"; - public const string Last = "last"; + keys.Add(new MetricBucket { Key = GetBucketKey(metricType, statName, current, interval, suffix), Time = current }); + current = current.Add(interval.Value); } + + return keys; + } + + private class CacheMetricNames + { + public const string Counter = "c"; + public const string Gauge = "g"; + public const string Timing = "t"; + + public const string Count = "cnt"; + public const string Total = "tot"; + public const string Max = "max"; + public const string Min = "min"; + public const string Last = "last"; } } diff --git a/src/Foundatio/Metrics/CounterStat.cs b/src/Foundatio/Metrics/CounterStat.cs index 6a1022830..d71ad0650 100644 --- a/src/Foundatio/Metrics/CounterStat.cs +++ b/src/Foundatio/Metrics/CounterStat.cs @@ -4,36 +4,35 @@ using System.Linq; using Foundatio.Utility; -namespace Foundatio.Metrics +namespace Foundatio.Metrics; + +[DebuggerDisplay("Time: {Time} Count: {Count}")] +public class CounterStat { - [DebuggerDisplay("Time: {Time} Count: {Count}")] - public class CounterStat - { - public DateTime Time { get; set; } - public long Count { get; set; } - } + public DateTime Time { get; set; } + public long Count { get; set; } +} - [DebuggerDisplay("Time: {StartTime}-{EndTime} Count: {Count}")] - public class CounterStatSummary +[DebuggerDisplay("Time: {StartTime}-{EndTime} Count: {Count}")] +public class CounterStatSummary +{ + public CounterStatSummary(string name, ICollection stats, DateTime start, DateTime end) { - public CounterStatSummary(string name, ICollection stats, DateTime start, DateTime end) - { - Name = name; - Stats.AddRange(stats); - Count = stats.Count > 0 ? Stats.Sum(s => s.Count) : 0; - StartTime = start; - EndTime = end; - } + Name = name; + Stats.AddRange(stats); + Count = stats.Count > 0 ? Stats.Sum(s => s.Count) : 0; + StartTime = start; + EndTime = end; + } - public string Name { get; private set; } - public DateTime StartTime { get; private set; } - public DateTime EndTime { get; private set; } - public ICollection Stats { get; } = new List(); - public long Count { get; private set; } + public string Name { get; private set; } + public DateTime StartTime { get; private set; } + public DateTime EndTime { get; private set; } + public ICollection Stats { get; } = new List(); + public long Count { get; private set; } - public override string ToString() - { - return $"Counter: {Name} Value: {Count}"; - } + public override string ToString() + { + return $"Counter: {Name} Value: {Count}"; } } diff --git a/src/Foundatio/Metrics/DiagnosticsMetricsClient.cs b/src/Foundatio/Metrics/DiagnosticsMetricsClient.cs index 0bfda25e7..add1ddd05 100644 --- a/src/Foundatio/Metrics/DiagnosticsMetricsClient.cs +++ b/src/Foundatio/Metrics/DiagnosticsMetricsClient.cs @@ -2,59 +2,58 @@ using System.Collections.Concurrent; using System.Diagnostics.Metrics; -namespace Foundatio.Metrics +namespace Foundatio.Metrics; + +public class DiagnosticsMetricsClient : IMetricsClient { - public class DiagnosticsMetricsClient : IMetricsClient - { - private readonly ConcurrentDictionary> _counters = new(); - private readonly ConcurrentDictionary _gauges = new(); - private readonly ConcurrentDictionary> _timers = new(); - private readonly Meter _meter; - private readonly string _prefix; + private readonly ConcurrentDictionary> _counters = new(); + private readonly ConcurrentDictionary _gauges = new(); + private readonly ConcurrentDictionary> _timers = new(); + private readonly Meter _meter; + private readonly string _prefix; - public DiagnosticsMetricsClient() : this(o => o) { } + public DiagnosticsMetricsClient() : this(o => o) { } - public DiagnosticsMetricsClient(DiagnosticsMetricsClientOptions options) - { - _prefix = !String.IsNullOrEmpty(options.Prefix) ? (!options.Prefix.EndsWith(".") ? options.Prefix + "." : options.Prefix) : String.Empty; - _meter = new Meter(options.MeterName ?? "Foundatio.MetricsClient", options.MeterVersion ?? FoundatioDiagnostics.AssemblyName.Version.ToString()); - } + public DiagnosticsMetricsClient(DiagnosticsMetricsClientOptions options) + { + _prefix = !String.IsNullOrEmpty(options.Prefix) ? (!options.Prefix.EndsWith(".") ? options.Prefix + "." : options.Prefix) : String.Empty; + _meter = new Meter(options.MeterName ?? "Foundatio.MetricsClient", options.MeterVersion ?? FoundatioDiagnostics.AssemblyName.Version.ToString()); + } - public DiagnosticsMetricsClient(Builder config) - : this(config(new DiagnosticsMetricsClientOptionsBuilder()).Build()) { } + public DiagnosticsMetricsClient(Builder config) + : this(config(new DiagnosticsMetricsClientOptionsBuilder()).Build()) { } - public void Counter(string name, int value = 1) - { - var counter = _counters.GetOrAdd(_prefix + name, _meter.CreateCounter(name)); - counter.Add(value); - } + public void Counter(string name, int value = 1) + { + var counter = _counters.GetOrAdd(_prefix + name, _meter.CreateCounter(name)); + counter.Add(value); + } - public void Gauge(string name, double value) - { - var gauge = _gauges.GetOrAdd(_prefix + name, new GaugeInfo(_meter, name)); - gauge.Value = value; - } + public void Gauge(string name, double value) + { + var gauge = _gauges.GetOrAdd(_prefix + name, new GaugeInfo(_meter, name)); + gauge.Value = value; + } - public void Timer(string name, int milliseconds) - { - var timer = _timers.GetOrAdd(_prefix + name, _meter.CreateHistogram(name, "ms")); - timer.Record(milliseconds); - } + public void Timer(string name, int milliseconds) + { + var timer = _timers.GetOrAdd(_prefix + name, _meter.CreateHistogram(name, "ms")); + timer.Record(milliseconds); + } - public void Dispose() - { - _meter.Dispose(); - } + public void Dispose() + { + _meter.Dispose(); + } - private class GaugeInfo + private class GaugeInfo + { + public GaugeInfo(Meter meter, string name) { - public GaugeInfo(Meter meter, string name) - { - Gauge = meter.CreateObservableGauge(name, () => Value); - } - - public ObservableGauge Gauge { get; } - public double Value { get; set; } + Gauge = meter.CreateObservableGauge(name, () => Value); } + + public ObservableGauge Gauge { get; } + public double Value { get; set; } } } diff --git a/src/Foundatio/Metrics/DiagnosticsMetricsClientOptions.cs b/src/Foundatio/Metrics/DiagnosticsMetricsClientOptions.cs index 6507b2508..3e02d7b8f 100644 --- a/src/Foundatio/Metrics/DiagnosticsMetricsClientOptions.cs +++ b/src/Foundatio/Metrics/DiagnosticsMetricsClientOptions.cs @@ -1,29 +1,28 @@ using System; -namespace Foundatio.Metrics +namespace Foundatio.Metrics; + +public class DiagnosticsMetricsClientOptions : SharedMetricsClientOptions { - public class DiagnosticsMetricsClientOptions : SharedMetricsClientOptions + public string MeterName { get; set; } + public string MeterVersion { get; set; } +} + +public class DiagnosticsMetricsClientOptionsBuilder : SharedMetricsClientOptionsBuilder +{ + public DiagnosticsMetricsClientOptionsBuilder MeterName(string name) { - public string MeterName { get; set; } - public string MeterVersion { get; set; } + if (String.IsNullOrEmpty(name)) + throw new ArgumentNullException(nameof(name)); + Target.MeterName = name; + return this; } - public class DiagnosticsMetricsClientOptionsBuilder : SharedMetricsClientOptionsBuilder + public DiagnosticsMetricsClientOptionsBuilder MeterVersion(string version) { - public DiagnosticsMetricsClientOptionsBuilder MeterName(string name) - { - if (String.IsNullOrEmpty(name)) - throw new ArgumentNullException(nameof(name)); - Target.MeterName = name; - return this; - } - - public DiagnosticsMetricsClientOptionsBuilder MeterVersion(string version) - { - if (String.IsNullOrEmpty(version)) - throw new ArgumentNullException(nameof(version)); - Target.MeterVersion = version; - return this; - } + if (String.IsNullOrEmpty(version)) + throw new ArgumentNullException(nameof(version)); + Target.MeterVersion = version; + return this; } } diff --git a/src/Foundatio/Metrics/GaugeStat.cs b/src/Foundatio/Metrics/GaugeStat.cs index 69b8b596e..92814269a 100644 --- a/src/Foundatio/Metrics/GaugeStat.cs +++ b/src/Foundatio/Metrics/GaugeStat.cs @@ -3,51 +3,50 @@ using System.Diagnostics; using System.Linq; -namespace Foundatio.Metrics +namespace Foundatio.Metrics; + +[DebuggerDisplay("Time: {Time} Max: {Max} Last: {Last}")] +public class GaugeStat { - [DebuggerDisplay("Time: {Time} Max: {Max} Last: {Last}")] - public class GaugeStat - { - public DateTime Time { get; set; } - public int Count { get; set; } - public double Total { get; set; } - public double Last { get; set; } - public double Min { get; set; } - public double Max { get; set; } - public double Average => Count > 0 ? Total / Count : 0; - } + public DateTime Time { get; set; } + public int Count { get; set; } + public double Total { get; set; } + public double Last { get; set; } + public double Min { get; set; } + public double Max { get; set; } + public double Average => Count > 0 ? Total / Count : 0; +} - [DebuggerDisplay("Time: {StartTime}-{EndTime} Max: {Max} Last: {Last}")] - public class GaugeStatSummary +[DebuggerDisplay("Time: {StartTime}-{EndTime} Max: {Max} Last: {Last}")] +public class GaugeStatSummary +{ + public GaugeStatSummary(string name, ICollection stats, DateTime start, DateTime end) { - public GaugeStatSummary(string name, ICollection stats, DateTime start, DateTime end) - { - Name = name; - Stats = stats; - Count = stats.Count > 0 ? Stats.Sum(s => s.Count) : 0; - Total = stats.Count > 0 ? Stats.Sum(s => s.Total) : 0; - Last = Stats.LastOrDefault()?.Last ?? 0; - Min = stats.Count > 0 ? Stats.Min(s => s.Min) : 0; - Max = stats.Count > 0 ? Stats.Max(s => s.Max) : 0; - StartTime = start; - EndTime = end; - Average = Count > 0 ? Total / Count : 0; - } + Name = name; + Stats = stats; + Count = stats.Count > 0 ? Stats.Sum(s => s.Count) : 0; + Total = stats.Count > 0 ? Stats.Sum(s => s.Total) : 0; + Last = Stats.LastOrDefault()?.Last ?? 0; + Min = stats.Count > 0 ? Stats.Min(s => s.Min) : 0; + Max = stats.Count > 0 ? Stats.Max(s => s.Max) : 0; + StartTime = start; + EndTime = end; + Average = Count > 0 ? Total / Count : 0; + } - public string Name { get; } - public DateTime StartTime { get; } - public DateTime EndTime { get; } - public ICollection Stats { get; } - public int Count { get; set; } - public double Total { get; set; } - public double Last { get; } - public double Min { get; set; } - public double Max { get; } - public double Average { get; } + public string Name { get; } + public DateTime StartTime { get; } + public DateTime EndTime { get; } + public ICollection Stats { get; } + public int Count { get; set; } + public double Total { get; set; } + public double Last { get; } + public double Min { get; set; } + public double Max { get; } + public double Average { get; } - public override string ToString() - { - return $"Counter: {Name} Time: {StartTime}-{EndTime} Max: {Max} Last: {Last}"; - } + public override string ToString() + { + return $"Counter: {Name} Time: {StartTime}-{EndTime} Max: {Max} Last: {Last}"; } } diff --git a/src/Foundatio/Metrics/IHaveSubMetricName.cs b/src/Foundatio/Metrics/IHaveSubMetricName.cs index 34e62ff00..0231e563c 100644 --- a/src/Foundatio/Metrics/IHaveSubMetricName.cs +++ b/src/Foundatio/Metrics/IHaveSubMetricName.cs @@ -1,7 +1,6 @@ -namespace Foundatio.Metrics +namespace Foundatio.Metrics; + +public interface IHaveSubMetricName { - public interface IHaveSubMetricName - { - string SubMetricName { get; } - } + string SubMetricName { get; } } diff --git a/src/Foundatio/Metrics/IMetricsClient.cs b/src/Foundatio/Metrics/IMetricsClient.cs index 75bf44ccd..c36fec8b4 100644 --- a/src/Foundatio/Metrics/IMetricsClient.cs +++ b/src/Foundatio/Metrics/IMetricsClient.cs @@ -2,44 +2,43 @@ using System.Threading.Tasks; using Foundatio.Utility; -namespace Foundatio.Metrics +namespace Foundatio.Metrics; + +[Obsolete("IMetricsClient will be removed, use System.Diagnostics.Metrics.Meter instead.")] +public interface IMetricsClient : IDisposable +{ + void Counter(string name, int value = 1); + void Gauge(string name, double value); + void Timer(string name, int milliseconds); +} + +public interface IBufferedMetricsClient : IMetricsClient { - [Obsolete("IMetricsClient will be removed, use System.Diagnostics.Metrics.Meter instead.")] - public interface IMetricsClient : IDisposable + Task FlushAsync(); +} + +public static class MetricsClientExtensions +{ + public static IDisposable StartTimer(this IMetricsClient client, string name) { - void Counter(string name, int value = 1); - void Gauge(string name, double value); - void Timer(string name, int milliseconds); + return new MetricTimer(name, client); } - public interface IBufferedMetricsClient : IMetricsClient + public static async Task TimeAsync(this IMetricsClient client, Func action, string name) { - Task FlushAsync(); + using (client.StartTimer(name)) + await action().AnyContext(); } - public static class MetricsClientExtensions + public static void Time(this IMetricsClient client, Action action, string name) { - public static IDisposable StartTimer(this IMetricsClient client, string name) - { - return new MetricTimer(name, client); - } - - public static async Task TimeAsync(this IMetricsClient client, Func action, string name) - { - using (client.StartTimer(name)) - await action().AnyContext(); - } - - public static void Time(this IMetricsClient client, Action action, string name) - { - using (client.StartTimer(name)) - action(); - } + using (client.StartTimer(name)) + action(); + } - public static async Task TimeAsync(this IMetricsClient client, Func> func, string name) - { - using (client.StartTimer(name)) - return await func().AnyContext(); - } + public static async Task TimeAsync(this IMetricsClient client, Func> func, string name) + { + using (client.StartTimer(name)) + return await func().AnyContext(); } } diff --git a/src/Foundatio/Metrics/IMetricsClientStats.cs b/src/Foundatio/Metrics/IMetricsClientStats.cs index d36cfe7e1..4a2bf1fff 100644 --- a/src/Foundatio/Metrics/IMetricsClientStats.cs +++ b/src/Foundatio/Metrics/IMetricsClientStats.cs @@ -3,60 +3,59 @@ using Foundatio.Queues; using Foundatio.Utility; -namespace Foundatio.Metrics +namespace Foundatio.Metrics; + +public interface IMetricsClientStats { - public interface IMetricsClientStats + Task GetCounterStatsAsync(string name, DateTime? utcStart = null, DateTime? utcEnd = null, int dataPoints = 20); + Task GetGaugeStatsAsync(string name, DateTime? utcStart = null, DateTime? utcEnd = null, int dataPoints = 20); + Task GetTimerStatsAsync(string name, DateTime? utcStart = null, DateTime? utcEnd = null, int dataPoints = 20); +} + +public static class MetricsClientStatsExtensions +{ + public static async Task GetCounterCountAsync(this IMetricsClientStats stats, string name, DateTime? utcStart = null, DateTime? utcEnd = null) { - Task GetCounterStatsAsync(string name, DateTime? utcStart = null, DateTime? utcEnd = null, int dataPoints = 20); - Task GetGaugeStatsAsync(string name, DateTime? utcStart = null, DateTime? utcEnd = null, int dataPoints = 20); - Task GetTimerStatsAsync(string name, DateTime? utcStart = null, DateTime? utcEnd = null, int dataPoints = 20); + var result = await stats.GetCounterStatsAsync(name, utcStart, utcEnd, 1).AnyContext(); + return result.Count; } - public static class MetricsClientStatsExtensions + public static async Task GetLastGaugeValueAsync(this IMetricsClientStats stats, string name, DateTime? utcStart = null, DateTime? utcEnd = null) { - public static async Task GetCounterCountAsync(this IMetricsClientStats stats, string name, DateTime? utcStart = null, DateTime? utcEnd = null) - { - var result = await stats.GetCounterStatsAsync(name, utcStart, utcEnd, 1).AnyContext(); - return result.Count; - } + var result = await stats.GetGaugeStatsAsync(name, utcStart, utcEnd, 1).AnyContext(); + return result.Last; + } - public static async Task GetLastGaugeValueAsync(this IMetricsClientStats stats, string name, DateTime? utcStart = null, DateTime? utcEnd = null) - { - var result = await stats.GetGaugeStatsAsync(name, utcStart, utcEnd, 1).AnyContext(); - return result.Last; - } + public static async Task GetQueueStatsAsync(this IMetricsClientStats stats, string name, string subMetricName = null, DateTime? utcStart = null, DateTime? utcEnd = null, int dataPoints = 20) + { + if (subMetricName == null) + subMetricName = String.Empty; + else + subMetricName = "." + subMetricName; + + var countTask = stats.GetGaugeStatsAsync($"{name}.count", utcStart, utcEnd, dataPoints); + var workingTask = stats.GetGaugeStatsAsync($"{name}.working", utcStart, utcEnd, dataPoints); + var deadletterTask = stats.GetGaugeStatsAsync($"{name}.deadletter", utcStart, utcEnd, dataPoints); + var enqueuedTask = stats.GetCounterStatsAsync($"{name}{subMetricName}.enqueued", utcStart, utcEnd, dataPoints); + var queueTimeTask = stats.GetTimerStatsAsync($"{name}{subMetricName}.queuetime", utcStart, utcEnd, dataPoints); + var dequeuedTask = stats.GetCounterStatsAsync($"{name}{subMetricName}.dequeued", utcStart, utcEnd, dataPoints); + var completedTask = stats.GetCounterStatsAsync($"{name}{subMetricName}.completed", utcStart, utcEnd, dataPoints); + var abandonedTask = stats.GetCounterStatsAsync($"{name}{subMetricName}.abandoned", utcStart, utcEnd, dataPoints); + var processTimeTask = stats.GetTimerStatsAsync($"{name}{subMetricName}.processtime", utcStart, utcEnd, dataPoints); + + await Task.WhenAll(countTask, workingTask, deadletterTask, enqueuedTask, queueTimeTask, dequeuedTask, completedTask, abandonedTask, processTimeTask).AnyContext(); - public static async Task GetQueueStatsAsync(this IMetricsClientStats stats, string name, string subMetricName = null, DateTime? utcStart = null, DateTime? utcEnd = null, int dataPoints = 20) + return new QueueStatSummary { - if (subMetricName == null) - subMetricName = String.Empty; - else - subMetricName = "." + subMetricName; - - var countTask = stats.GetGaugeStatsAsync($"{name}.count", utcStart, utcEnd, dataPoints); - var workingTask = stats.GetGaugeStatsAsync($"{name}.working", utcStart, utcEnd, dataPoints); - var deadletterTask = stats.GetGaugeStatsAsync($"{name}.deadletter", utcStart, utcEnd, dataPoints); - var enqueuedTask = stats.GetCounterStatsAsync($"{name}{subMetricName}.enqueued", utcStart, utcEnd, dataPoints); - var queueTimeTask = stats.GetTimerStatsAsync($"{name}{subMetricName}.queuetime", utcStart, utcEnd, dataPoints); - var dequeuedTask = stats.GetCounterStatsAsync($"{name}{subMetricName}.dequeued", utcStart, utcEnd, dataPoints); - var completedTask = stats.GetCounterStatsAsync($"{name}{subMetricName}.completed", utcStart, utcEnd, dataPoints); - var abandonedTask = stats.GetCounterStatsAsync($"{name}{subMetricName}.abandoned", utcStart, utcEnd, dataPoints); - var processTimeTask = stats.GetTimerStatsAsync($"{name}{subMetricName}.processtime", utcStart, utcEnd, dataPoints); - - await Task.WhenAll(countTask, workingTask, deadletterTask, enqueuedTask, queueTimeTask, dequeuedTask, completedTask, abandonedTask, processTimeTask).AnyContext(); - - return new QueueStatSummary - { - Count = countTask.Result, - Working = workingTask.Result, - Deadletter = deadletterTask.Result, - Enqueued = enqueuedTask.Result, - QueueTime = queueTimeTask.Result, - Dequeued = dequeuedTask.Result, - Completed = completedTask.Result, - Abandoned = abandonedTask.Result, - ProcessTime = processTimeTask.Result - }; - } + Count = countTask.Result, + Working = workingTask.Result, + Deadletter = deadletterTask.Result, + Enqueued = enqueuedTask.Result, + QueueTime = queueTimeTask.Result, + Dequeued = dequeuedTask.Result, + Completed = completedTask.Result, + Abandoned = abandonedTask.Result, + ProcessTime = processTimeTask.Result + }; } } diff --git a/src/Foundatio/Metrics/InMemoryMetricsClient.cs b/src/Foundatio/Metrics/InMemoryMetricsClient.cs index 12ef43271..8a89c4813 100644 --- a/src/Foundatio/Metrics/InMemoryMetricsClient.cs +++ b/src/Foundatio/Metrics/InMemoryMetricsClient.cs @@ -1,22 +1,21 @@ using System; using Foundatio.Caching; -namespace Foundatio.Metrics +namespace Foundatio.Metrics; + +public class InMemoryMetricsClient : CacheBucketMetricsClientBase { - public class InMemoryMetricsClient : CacheBucketMetricsClientBase - { - public InMemoryMetricsClient() : this(o => o) { } + public InMemoryMetricsClient() : this(o => o) { } - public InMemoryMetricsClient(InMemoryMetricsClientOptions options) - : base(new InMemoryCacheClient(new InMemoryCacheClientOptions { LoggerFactory = options?.LoggerFactory }), options) { } + public InMemoryMetricsClient(InMemoryMetricsClientOptions options) + : base(new InMemoryCacheClient(new InMemoryCacheClientOptions { LoggerFactory = options?.LoggerFactory }), options) { } - public InMemoryMetricsClient(Builder config) - : this(config(new InMemoryMetricsClientOptionsBuilder()).Build()) { } + public InMemoryMetricsClient(Builder config) + : this(config(new InMemoryMetricsClientOptionsBuilder()).Build()) { } - public override void Dispose() - { - base.Dispose(); - _cache.Dispose(); - } + public override void Dispose() + { + base.Dispose(); + _cache.Dispose(); } } diff --git a/src/Foundatio/Metrics/InMemoryMetricsClientOptions.cs b/src/Foundatio/Metrics/InMemoryMetricsClientOptions.cs index 9687aa3d2..c4444de3b 100644 --- a/src/Foundatio/Metrics/InMemoryMetricsClientOptions.cs +++ b/src/Foundatio/Metrics/InMemoryMetricsClientOptions.cs @@ -1,6 +1,5 @@ -namespace Foundatio.Metrics -{ - public class InMemoryMetricsClientOptions : SharedMetricsClientOptions { } +namespace Foundatio.Metrics; - public class InMemoryMetricsClientOptionsBuilder : SharedMetricsClientOptionsBuilder { } -} +public class InMemoryMetricsClientOptions : SharedMetricsClientOptions { } + +public class InMemoryMetricsClientOptionsBuilder : SharedMetricsClientOptionsBuilder { } diff --git a/src/Foundatio/Metrics/MetricKey.cs b/src/Foundatio/Metrics/MetricKey.cs index 1887280c0..2d27055e1 100644 --- a/src/Foundatio/Metrics/MetricKey.cs +++ b/src/Foundatio/Metrics/MetricKey.cs @@ -1,51 +1,50 @@ using System; -namespace Foundatio.Metrics +namespace Foundatio.Metrics; + +public struct MetricKey : IEquatable { - public struct MetricKey : IEquatable + public MetricKey(DateTime startTimeUtc, TimeSpan duration, string name) { - public MetricKey(DateTime startTimeUtc, TimeSpan duration, string name) - { - StartTimeUtc = startTimeUtc; - Duration = duration; - Name = name; - } + StartTimeUtc = startTimeUtc; + Duration = duration; + Name = name; + } - public DateTime StartTimeUtc { get; } - public TimeSpan Duration { get; } - public string Name { get; } + public DateTime StartTimeUtc { get; } + public TimeSpan Duration { get; } + public string Name { get; } - public DateTime EndTimeUtc => StartTimeUtc.Add(Duration); + public DateTime EndTimeUtc => StartTimeUtc.Add(Duration); - public bool Equals(MetricKey other) - { - return StartTimeUtc == other.StartTimeUtc && Duration == other.Duration && String.Equals(Name, other.Name); - } + public bool Equals(MetricKey other) + { + return StartTimeUtc == other.StartTimeUtc && Duration == other.Duration && String.Equals(Name, other.Name); + } - public override bool Equals(object obj) - { - if (obj is null) - return false; + public override bool Equals(object obj) + { + if (obj is null) + return false; - return obj is MetricKey key && Equals(key); - } + return obj is MetricKey key && Equals(key); + } - public override int GetHashCode() + public override int GetHashCode() + { + unchecked { - unchecked - { - return (StartTimeUtc.GetHashCode() * 397) ^ (Duration.GetHashCode() * 397) ^ (Name?.GetHashCode() ?? 0); - } + return (StartTimeUtc.GetHashCode() * 397) ^ (Duration.GetHashCode() * 397) ^ (Name?.GetHashCode() ?? 0); } + } - public static bool operator ==(MetricKey left, MetricKey right) - { - return left.Equals(right); - } + public static bool operator ==(MetricKey left, MetricKey right) + { + return left.Equals(right); + } - public static bool operator !=(MetricKey left, MetricKey right) - { - return !left.Equals(right); - } + public static bool operator !=(MetricKey left, MetricKey right) + { + return !left.Equals(right); } } diff --git a/src/Foundatio/Metrics/MetricTimer.cs b/src/Foundatio/Metrics/MetricTimer.cs index b578a838d..8f6df7514 100644 --- a/src/Foundatio/Metrics/MetricTimer.cs +++ b/src/Foundatio/Metrics/MetricTimer.cs @@ -3,30 +3,29 @@ using System.Threading.Tasks; using Foundatio.Utility; -namespace Foundatio.Metrics +namespace Foundatio.Metrics; + +public class MetricTimer : IDisposable { - public class MetricTimer : IDisposable - { - private readonly string _name; - private readonly Stopwatch _stopWatch; - private bool _disposed; - private readonly IMetricsClient _client; + private readonly string _name; + private readonly Stopwatch _stopWatch; + private bool _disposed; + private readonly IMetricsClient _client; - public MetricTimer(string name, IMetricsClient client) - { - _name = name; - _client = client; - _stopWatch = Stopwatch.StartNew(); - } + public MetricTimer(string name, IMetricsClient client) + { + _name = name; + _client = client; + _stopWatch = Stopwatch.StartNew(); + } - public void Dispose() - { - if (_disposed) - return; + public void Dispose() + { + if (_disposed) + return; - _disposed = true; - _stopWatch.Stop(); - _client.Timer(_name, (int)_stopWatch.ElapsedMilliseconds); - } + _disposed = true; + _stopWatch.Stop(); + _client.Timer(_name, (int)_stopWatch.ElapsedMilliseconds); } } diff --git a/src/Foundatio/Metrics/NullMetricsClient.cs b/src/Foundatio/Metrics/NullMetricsClient.cs index 8f1ebab0f..c7b109be0 100644 --- a/src/Foundatio/Metrics/NullMetricsClient.cs +++ b/src/Foundatio/Metrics/NullMetricsClient.cs @@ -1,13 +1,12 @@ using System.Threading.Tasks; -namespace Foundatio.Metrics +namespace Foundatio.Metrics; + +public class NullMetricsClient : IMetricsClient { - public class NullMetricsClient : IMetricsClient - { - public static readonly IMetricsClient Instance = new NullMetricsClient(); - public void Counter(string name, int value = 1) { } - public void Gauge(string name, double value) { } - public void Timer(string name, int milliseconds) { } - public void Dispose() { } - } + public static readonly IMetricsClient Instance = new NullMetricsClient(); + public void Counter(string name, int value = 1) { } + public void Gauge(string name, double value) { } + public void Timer(string name, int milliseconds) { } + public void Dispose() { } } diff --git a/src/Foundatio/Metrics/SharedMetricsClientOptions.cs b/src/Foundatio/Metrics/SharedMetricsClientOptions.cs index 1af0cd8a8..0586ac6dc 100644 --- a/src/Foundatio/Metrics/SharedMetricsClientOptions.cs +++ b/src/Foundatio/Metrics/SharedMetricsClientOptions.cs @@ -1,31 +1,30 @@ using System; -namespace Foundatio.Metrics +namespace Foundatio.Metrics; + +public class SharedMetricsClientOptions : SharedOptions +{ + public bool Buffered { get; set; } = true; + public string Prefix { get; set; } +} + +public class SharedMetricsClientOptionsBuilder : SharedOptionsBuilder + where TOption : SharedMetricsClientOptions, new() + where TBuilder : SharedMetricsClientOptionsBuilder { - public class SharedMetricsClientOptions : SharedOptions + public TBuilder Buffered(bool buffered) { - public bool Buffered { get; set; } = true; - public string Prefix { get; set; } + Target.Buffered = buffered; + return (TBuilder)this; } - public class SharedMetricsClientOptionsBuilder : SharedOptionsBuilder - where TOption : SharedMetricsClientOptions, new() - where TBuilder : SharedMetricsClientOptionsBuilder + public TBuilder Prefix(string prefix) { - public TBuilder Buffered(bool buffered) - { - Target.Buffered = buffered; - return (TBuilder)this; - } - - public TBuilder Prefix(string prefix) - { - Target.Prefix = prefix; - return (TBuilder)this; - } + Target.Prefix = prefix; + return (TBuilder)this; + } - public TBuilder EnableBuffer() => Buffered(true); + public TBuilder EnableBuffer() => Buffered(true); - public TBuilder DisableBuffer() => Buffered(false); - } + public TBuilder DisableBuffer() => Buffered(false); } diff --git a/src/Foundatio/Metrics/StatsDMetricsClient.cs b/src/Foundatio/Metrics/StatsDMetricsClient.cs index 813db11b9..620eae4b0 100644 --- a/src/Foundatio/Metrics/StatsDMetricsClient.cs +++ b/src/Foundatio/Metrics/StatsDMetricsClient.cs @@ -8,145 +8,144 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -namespace Foundatio.Metrics +namespace Foundatio.Metrics; + +[SuppressMessage("ReSharper", "InconsistentlySynchronizedField")] +[Obsolete("StatsDMetricsClient will be removed, use System.Diagnostics.Metrics.Meter instead.")] +public class StatsDMetricsClient : IMetricsClient { - [SuppressMessage("ReSharper", "InconsistentlySynchronizedField")] - [Obsolete("StatsDMetricsClient will be removed, use System.Diagnostics.Metrics.Meter instead.")] - public class StatsDMetricsClient : IMetricsClient + private readonly object _lock = new(); + private Socket _socket; + private readonly IPEndPoint _endPoint; + private readonly StatsDMetricsClientOptions _options; + private readonly ILogger _logger; + + public StatsDMetricsClient(StatsDMetricsClientOptions options) { - private readonly object _lock = new(); - private Socket _socket; - private readonly IPEndPoint _endPoint; - private readonly StatsDMetricsClientOptions _options; - private readonly ILogger _logger; + _options = options; + _logger = options.LoggerFactory?.CreateLogger() ?? NullLogger.Instance; + _endPoint = GetIPEndPointFromHostName(options.ServerName, options.Port, false); - public StatsDMetricsClient(StatsDMetricsClientOptions options) - { - _options = options; - _logger = options.LoggerFactory?.CreateLogger() ?? NullLogger.Instance; - _endPoint = GetIPEndPointFromHostName(options.ServerName, options.Port, false); + if (!String.IsNullOrEmpty(options.Prefix)) + options.Prefix = options.Prefix.EndsWith(".") ? options.Prefix : String.Concat(options.Prefix, "."); + } - if (!String.IsNullOrEmpty(options.Prefix)) - options.Prefix = options.Prefix.EndsWith(".") ? options.Prefix : String.Concat(options.Prefix, "."); - } + public StatsDMetricsClient(Builder config) + : this(config(new StatsDMetricsClientOptionsBuilder()).Build()) { } - public StatsDMetricsClient(Builder config) - : this(config(new StatsDMetricsClientOptionsBuilder()).Build()) { } + public void Counter(string name, int value = 1) + { + Send(BuildMetric("c", name, value.ToString(CultureInfo.InvariantCulture))); + } - public void Counter(string name, int value = 1) - { - Send(BuildMetric("c", name, value.ToString(CultureInfo.InvariantCulture))); - } + public void Gauge(string name, double value) + { + Send(BuildMetric("g", name, value.ToString(CultureInfo.InvariantCulture))); + } - public void Gauge(string name, double value) - { - Send(BuildMetric("g", name, value.ToString(CultureInfo.InvariantCulture))); - } + public void Timer(string name, int milliseconds) + { + Send(BuildMetric("ms", name, milliseconds.ToString(CultureInfo.InvariantCulture))); + } - public void Timer(string name, int milliseconds) - { - Send(BuildMetric("ms", name, milliseconds.ToString(CultureInfo.InvariantCulture))); - } + private string BuildMetric(string type, string statName, string value) + { + return String.Concat(_options.Prefix, statName, ":", value, "|", type); + } - private string BuildMetric(string type, string statName, string value) - { - return String.Concat(_options.Prefix, statName, ":", value, "|", type); - } + private void Send(string metric) + { + if (String.IsNullOrEmpty(metric)) + return; - private void Send(string metric) + try { - if (String.IsNullOrEmpty(metric)) - return; + var data = Encoding.ASCII.GetBytes(metric); - try - { - var data = Encoding.ASCII.GetBytes(metric); - - EnsureSocket(); - lock (_lock) - { - _logger.LogTrace("Sending metric: {Metric}", metric); - _socket.SendTo(data, _endPoint); - } - } - catch (Exception ex) + EnsureSocket(); + lock (_lock) { - _logger.LogError(ex, "An error occurred while sending the metrics: {Message}", ex.Message); - CloseSocket(); + _logger.LogTrace("Sending metric: {Metric}", metric); + _socket.SendTo(data, _endPoint); } } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred while sending the metrics: {Message}", ex.Message); + CloseSocket(); + } + } + + private void EnsureSocket() + { + _logger.LogTrace("EnsureSocket"); + if (_socket != null) + return; - private void EnsureSocket() + lock (_lock) { - _logger.LogTrace("EnsureSocket"); if (_socket != null) return; - lock (_lock) - { - if (_socket != null) - return; - - _logger.LogTrace("Creating socket"); - _socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); - if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - _socket.SendBufferSize = 0; - } + _logger.LogTrace("Creating socket"); + _socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + _socket.SendBufferSize = 0; } + } - private void CloseSocket() - { - _logger.LogTrace("CloseSocket"); + private void CloseSocket() + { + _logger.LogTrace("CloseSocket"); + + if (_socket == null) + return; + lock (_lock) + { if (_socket == null) return; - lock (_lock) + _logger.LogTrace("Closing socket"); + try { - if (_socket == null) - return; - - _logger.LogTrace("Closing socket"); - try - { - _socket.Close(); - } - catch (Exception ex) - { - _logger.LogError(ex, "An error occurred while calling Close() on the socket"); - } - finally - { - _socket = null; - } + _socket.Close(); } - } - - private IPEndPoint GetIPEndPointFromHostName(string hostName, int port, bool throwIfMoreThanOneIP) - { - var addresses = Dns.GetHostAddresses(hostName); - if (addresses.Length == 0) + catch (Exception ex) { - throw new ArgumentException( - "Unable to retrieve address from specified host name.", - nameof(hostName) - ); + _logger.LogError(ex, "An error occurred while calling Close() on the socket"); } - - if (throwIfMoreThanOneIP && addresses.Length > 1) + finally { - throw new ArgumentException( - "There is more that one IP address to the specified host.", - nameof(hostName) - ); + _socket = null; } + } + } - return new IPEndPoint(addresses[0], port); + private IPEndPoint GetIPEndPointFromHostName(string hostName, int port, bool throwIfMoreThanOneIP) + { + var addresses = Dns.GetHostAddresses(hostName); + if (addresses.Length == 0) + { + throw new ArgumentException( + "Unable to retrieve address from specified host name.", + nameof(hostName) + ); } - public void Dispose() + if (throwIfMoreThanOneIP && addresses.Length > 1) { - CloseSocket(); + throw new ArgumentException( + "There is more that one IP address to the specified host.", + nameof(hostName) + ); } + + return new IPEndPoint(addresses[0], port); + } + + public void Dispose() + { + CloseSocket(); } } diff --git a/src/Foundatio/Metrics/StatsDMetricsClientOptions.cs b/src/Foundatio/Metrics/StatsDMetricsClientOptions.cs index 6bbd0116f..53a5ccd62 100644 --- a/src/Foundatio/Metrics/StatsDMetricsClientOptions.cs +++ b/src/Foundatio/Metrics/StatsDMetricsClientOptions.cs @@ -1,22 +1,21 @@ using System; -namespace Foundatio.Metrics +namespace Foundatio.Metrics; + +public class StatsDMetricsClientOptions : SharedMetricsClientOptions { - public class StatsDMetricsClientOptions : SharedMetricsClientOptions - { - public string ServerName { get; set; } - public int Port { get; set; } = 8125; - } + public string ServerName { get; set; } + public int Port { get; set; } = 8125; +} - public class StatsDMetricsClientOptionsBuilder : SharedMetricsClientOptionsBuilder +public class StatsDMetricsClientOptionsBuilder : SharedMetricsClientOptionsBuilder +{ + public StatsDMetricsClientOptionsBuilder Server(string serverName, int port = 8125) { - public StatsDMetricsClientOptionsBuilder Server(string serverName, int port = 8125) - { - if (String.IsNullOrEmpty(serverName)) - throw new ArgumentNullException(nameof(serverName)); - Target.ServerName = serverName; - Target.Port = port; - return this; - } + if (String.IsNullOrEmpty(serverName)) + throw new ArgumentNullException(nameof(serverName)); + Target.ServerName = serverName; + Target.Port = port; + return this; } } diff --git a/src/Foundatio/Metrics/TimingStat.cs b/src/Foundatio/Metrics/TimingStat.cs index 2f51b3aea..00023e915 100644 --- a/src/Foundatio/Metrics/TimingStat.cs +++ b/src/Foundatio/Metrics/TimingStat.cs @@ -3,48 +3,47 @@ using System.Diagnostics; using System.Linq; -namespace Foundatio.Metrics +namespace Foundatio.Metrics; + +[DebuggerDisplay("Time: {Time} Count: {Count} Min: {MinDuration} Max: {MaxDuration} Total: {TotalDuration} Avg: {AverageDuration}")] +public class TimingStat { - [DebuggerDisplay("Time: {Time} Count: {Count} Min: {MinDuration} Max: {MaxDuration} Total: {TotalDuration} Avg: {AverageDuration}")] - public class TimingStat - { - public DateTime Time { get; set; } - public int Count { get; set; } - public long TotalDuration { get; set; } - public int MinDuration { get; set; } - public int MaxDuration { get; set; } - public double AverageDuration => Count > 0 ? (double)TotalDuration / Count : 0; - } + public DateTime Time { get; set; } + public int Count { get; set; } + public long TotalDuration { get; set; } + public int MinDuration { get; set; } + public int MaxDuration { get; set; } + public double AverageDuration => Count > 0 ? (double)TotalDuration / Count : 0; +} - [DebuggerDisplay("Time: {StartTime}-{EndTime} Count: {Count} Min: {MinDuration} Max: {MaxDuration} Total: {TotalDuration} Avg: {AverageDuration}")] - public class TimingStatSummary +[DebuggerDisplay("Time: {StartTime}-{EndTime} Count: {Count} Min: {MinDuration} Max: {MaxDuration} Total: {TotalDuration} Avg: {AverageDuration}")] +public class TimingStatSummary +{ + public TimingStatSummary(string name, ICollection stats, DateTime start, DateTime end) { - public TimingStatSummary(string name, ICollection stats, DateTime start, DateTime end) - { - Name = name; - Stats = stats; - Count = stats.Count > 0 ? Stats.Sum(s => s.Count) : 0; - MinDuration = stats.Count > 0 ? Stats.Min(s => s.MinDuration) : 0; - MaxDuration = stats.Count > 0 ? Stats.Max(s => s.MaxDuration) : 0; - TotalDuration = stats.Count > 0 ? Stats.Sum(s => s.TotalDuration) : 0; - AverageDuration = Count > 0 ? (double)TotalDuration / Count : 0; - StartTime = start; - EndTime = end; - } + Name = name; + Stats = stats; + Count = stats.Count > 0 ? Stats.Sum(s => s.Count) : 0; + MinDuration = stats.Count > 0 ? Stats.Min(s => s.MinDuration) : 0; + MaxDuration = stats.Count > 0 ? Stats.Max(s => s.MaxDuration) : 0; + TotalDuration = stats.Count > 0 ? Stats.Sum(s => s.TotalDuration) : 0; + AverageDuration = Count > 0 ? (double)TotalDuration / Count : 0; + StartTime = start; + EndTime = end; + } - public string Name { get; } - public DateTime StartTime { get; } - public DateTime EndTime { get; } - public ICollection Stats { get; } - public int Count { get; } - public int MinDuration { get; } - public int MaxDuration { get; } - public long TotalDuration { get; } - public double AverageDuration { get; } + public string Name { get; } + public DateTime StartTime { get; } + public DateTime EndTime { get; } + public ICollection Stats { get; } + public int Count { get; } + public int MinDuration { get; } + public int MaxDuration { get; } + public long TotalDuration { get; } + public double AverageDuration { get; } - public override string ToString() - { - return $"Timing: {Name} Time: {StartTime}-{EndTime} Count: {Count} Min: {MinDuration} Max: {MaxDuration} Total: {TotalDuration} Avg: {AverageDuration}"; - } + public override string ToString() + { + return $"Timing: {Name} Time: {StartTime}-{EndTime} Count: {Count} Min: {MinDuration} Max: {MaxDuration} Total: {TotalDuration} Avg: {AverageDuration}"; } } diff --git a/src/Foundatio/Nito.AsyncEx.Coordination/AsyncAutoResetEvent.cs b/src/Foundatio/Nito.AsyncEx.Coordination/AsyncAutoResetEvent.cs index 19b7405d0..224a57cba 100644 --- a/src/Foundatio/Nito.AsyncEx.Coordination/AsyncAutoResetEvent.cs +++ b/src/Foundatio/Nito.AsyncEx.Coordination/AsyncAutoResetEvent.cs @@ -6,159 +6,158 @@ // Original idea by Stephen Toub: http://blogs.msdn.com/b/pfxteam/archive/2012/02/11/10266923.aspx -namespace Foundatio.AsyncEx +namespace Foundatio.AsyncEx; + +/// +/// An async-compatible auto-reset event. +/// +[DebuggerDisplay("Id = {Id}, IsSet = {_set}")] +[DebuggerTypeProxy(typeof(DebugView))] +public sealed class AsyncAutoResetEvent { /// - /// An async-compatible auto-reset event. + /// The queue of TCSs that other tasks are awaiting. /// - [DebuggerDisplay("Id = {Id}, IsSet = {_set}")] - [DebuggerTypeProxy(typeof(DebugView))] - public sealed class AsyncAutoResetEvent + private readonly IAsyncWaitQueue _queue; + + /// + /// The current state of the event. + /// + private bool _set; + + /// + /// The semi-unique identifier for this instance. This is 0 if the id has not yet been created. + /// + private int _id; + + /// + /// The object used for mutual exclusion. + /// + private readonly object _mutex; + + /// + /// Creates an async-compatible auto-reset event. + /// + /// Whether the auto-reset event is initially set or unset. + /// The wait queue used to manage waiters. This may be null to use a default (FIFO) queue. + internal AsyncAutoResetEvent(bool set, IAsyncWaitQueue queue) { - /// - /// The queue of TCSs that other tasks are awaiting. - /// - private readonly IAsyncWaitQueue _queue; - - /// - /// The current state of the event. - /// - private bool _set; - - /// - /// The semi-unique identifier for this instance. This is 0 if the id has not yet been created. - /// - private int _id; - - /// - /// The object used for mutual exclusion. - /// - private readonly object _mutex; - - /// - /// Creates an async-compatible auto-reset event. - /// - /// Whether the auto-reset event is initially set or unset. - /// The wait queue used to manage waiters. This may be null to use a default (FIFO) queue. - internal AsyncAutoResetEvent(bool set, IAsyncWaitQueue queue) - { - _queue = queue ?? new DefaultAsyncWaitQueue(); - _set = set; - _mutex = new object(); - } + _queue = queue ?? new DefaultAsyncWaitQueue(); + _set = set; + _mutex = new object(); + } - /// - /// Creates an async-compatible auto-reset event. - /// - /// Whether the auto-reset event is initially set or unset. - public AsyncAutoResetEvent(bool set) - : this(set, null) - { - } + /// + /// Creates an async-compatible auto-reset event. + /// + /// Whether the auto-reset event is initially set or unset. + public AsyncAutoResetEvent(bool set) + : this(set, null) + { + } - /// - /// Creates an async-compatible auto-reset event that is initially unset. - /// - public AsyncAutoResetEvent() - : this(false, null) - { - } + /// + /// Creates an async-compatible auto-reset event that is initially unset. + /// + public AsyncAutoResetEvent() + : this(false, null) + { + } - /// - /// Gets a semi-unique identifier for this asynchronous auto-reset event. - /// - public int Id - { - get { return IdManager.GetId(ref _id); } - } + /// + /// Gets a semi-unique identifier for this asynchronous auto-reset event. + /// + public int Id + { + get { return IdManager.GetId(ref _id); } + } - /// - /// Whether this event is currently set. This member is seldom used; code using this member has a high possibility of race conditions. - /// - public bool IsSet - { - get { lock (_mutex) return _set; } - } + /// + /// Whether this event is currently set. This member is seldom used; code using this member has a high possibility of race conditions. + /// + public bool IsSet + { + get { lock (_mutex) return _set; } + } - /// - /// Asynchronously waits for this event to be set. If the event is set, this method will auto-reset it and return immediately, even if the cancellation token is already signalled. If the wait is canceled, then it will not auto-reset this event. - /// - /// The cancellation token used to cancel this wait. - public Task WaitAsync(CancellationToken cancellationToken) + /// + /// Asynchronously waits for this event to be set. If the event is set, this method will auto-reset it and return immediately, even if the cancellation token is already signalled. If the wait is canceled, then it will not auto-reset this event. + /// + /// The cancellation token used to cancel this wait. + public Task WaitAsync(CancellationToken cancellationToken) + { + Task ret; + lock (_mutex) { - Task ret; - lock (_mutex) + if (_set) { - if (_set) - { - _set = false; - ret = TaskConstants.Completed; - } - else - { - ret = _queue.Enqueue(_mutex, cancellationToken); - } + _set = false; + ret = TaskConstants.Completed; + } + else + { + ret = _queue.Enqueue(_mutex, cancellationToken); } - - return ret; } - /// - /// Asynchronously waits for this event to be set. If the event is set, this method will auto-reset it and return immediately. - /// - public Task WaitAsync() - { - return WaitAsync(CancellationToken.None); - } + return ret; + } - /// - /// Synchronously waits for this event to be set. If the event is set, this method will auto-reset it and return immediately, even if the cancellation token is already signalled. If the wait is canceled, then it will not auto-reset this event. This method may block the calling thread. - /// - /// The cancellation token used to cancel this wait. - public void Wait(CancellationToken cancellationToken) - { - WaitAsync(cancellationToken).WaitAndUnwrapException(); - } + /// + /// Asynchronously waits for this event to be set. If the event is set, this method will auto-reset it and return immediately. + /// + public Task WaitAsync() + { + return WaitAsync(CancellationToken.None); + } - /// - /// Synchronously waits for this event to be set. If the event is set, this method will auto-reset it and return immediately. This method may block the calling thread. - /// - public void Wait() - { - Wait(CancellationToken.None); - } + /// + /// Synchronously waits for this event to be set. If the event is set, this method will auto-reset it and return immediately, even if the cancellation token is already signalled. If the wait is canceled, then it will not auto-reset this event. This method may block the calling thread. + /// + /// The cancellation token used to cancel this wait. + public void Wait(CancellationToken cancellationToken) + { + WaitAsync(cancellationToken).WaitAndUnwrapException(); + } + + /// + /// Synchronously waits for this event to be set. If the event is set, this method will auto-reset it and return immediately. This method may block the calling thread. + /// + public void Wait() + { + Wait(CancellationToken.None); + } - /// - /// Sets the event, atomically completing a task returned by . If the event is already set, this method does nothing. - /// - public void Set() + /// + /// Sets the event, atomically completing a task returned by . If the event is already set, this method does nothing. + /// + public void Set() + { + lock (_mutex) { - lock (_mutex) - { - if (_queue.IsEmpty) - _set = true; - else - _queue.Dequeue(); - } + if (_queue.IsEmpty) + _set = true; + else + _queue.Dequeue(); } + } - // ReSharper disable UnusedMember.Local - [DebuggerNonUserCode] - private sealed class DebugView - { - private readonly AsyncAutoResetEvent _are; + // ReSharper disable UnusedMember.Local + [DebuggerNonUserCode] + private sealed class DebugView + { + private readonly AsyncAutoResetEvent _are; - public DebugView(AsyncAutoResetEvent are) - { - _are = are; - } + public DebugView(AsyncAutoResetEvent are) + { + _are = are; + } - public int Id { get { return _are.Id; } } + public int Id { get { return _are.Id; } } - public bool IsSet { get { return _are._set; } } + public bool IsSet { get { return _are._set; } } - public IAsyncWaitQueue WaitQueue { get { return _are._queue; } } - } - // ReSharper restore UnusedMember.Local + public IAsyncWaitQueue WaitQueue { get { return _are._queue; } } } + // ReSharper restore UnusedMember.Local } diff --git a/src/Foundatio/Nito.AsyncEx.Coordination/AsyncConditionVariable.cs b/src/Foundatio/Nito.AsyncEx.Coordination/AsyncConditionVariable.cs index 9a449e4f8..5e5075dfa 100644 --- a/src/Foundatio/Nito.AsyncEx.Coordination/AsyncConditionVariable.cs +++ b/src/Foundatio/Nito.AsyncEx.Coordination/AsyncConditionVariable.cs @@ -4,164 +4,163 @@ using System.Threading.Tasks; using Foundatio.AsyncEx.Synchronous; -namespace Foundatio.AsyncEx +namespace Foundatio.AsyncEx; + +/// +/// An async-compatible condition variable. This type uses Mesa-style semantics (the notifying tasks do not yield). +/// +[DebuggerDisplay("Id = {Id}, AsyncLockId = {_asyncLock.Id}")] +[DebuggerTypeProxy(typeof(DebugView))] +public sealed class AsyncConditionVariable { /// - /// An async-compatible condition variable. This type uses Mesa-style semantics (the notifying tasks do not yield). + /// The lock associated with this condition variable. /// - [DebuggerDisplay("Id = {Id}, AsyncLockId = {_asyncLock.Id}")] - [DebuggerTypeProxy(typeof(DebugView))] - public sealed class AsyncConditionVariable + private readonly AsyncLock _asyncLock; + + /// + /// The queue of waiting tasks. + /// + private readonly IAsyncWaitQueue _queue; + + /// + /// The semi-unique identifier for this instance. This is 0 if the id has not yet been created. + /// + private int _id; + + /// + /// The object used for mutual exclusion. + /// + private readonly object _mutex; + + /// + /// Creates an async-compatible condition variable associated with an async-compatible lock. + /// + /// The lock associated with this condition variable. + /// The wait queue used to manage waiters. This may be null to use a default (FIFO) queue. + internal AsyncConditionVariable(AsyncLock asyncLock, IAsyncWaitQueue queue) { - /// - /// The lock associated with this condition variable. - /// - private readonly AsyncLock _asyncLock; - - /// - /// The queue of waiting tasks. - /// - private readonly IAsyncWaitQueue _queue; - - /// - /// The semi-unique identifier for this instance. This is 0 if the id has not yet been created. - /// - private int _id; - - /// - /// The object used for mutual exclusion. - /// - private readonly object _mutex; - - /// - /// Creates an async-compatible condition variable associated with an async-compatible lock. - /// - /// The lock associated with this condition variable. - /// The wait queue used to manage waiters. This may be null to use a default (FIFO) queue. - internal AsyncConditionVariable(AsyncLock asyncLock, IAsyncWaitQueue queue) - { - _asyncLock = asyncLock; - _queue = queue ?? new DefaultAsyncWaitQueue(); - _mutex = new object(); - } + _asyncLock = asyncLock; + _queue = queue ?? new DefaultAsyncWaitQueue(); + _mutex = new object(); + } - /// - /// Creates an async-compatible condition variable associated with an async-compatible lock. - /// - /// The lock associated with this condition variable. - public AsyncConditionVariable(AsyncLock asyncLock) - : this(asyncLock, null) - { - } + /// + /// Creates an async-compatible condition variable associated with an async-compatible lock. + /// + /// The lock associated with this condition variable. + public AsyncConditionVariable(AsyncLock asyncLock) + : this(asyncLock, null) + { + } - /// - /// Gets a semi-unique identifier for this asynchronous condition variable. - /// - public int Id - { - get { return IdManager.GetId(ref _id); } - } + /// + /// Gets a semi-unique identifier for this asynchronous condition variable. + /// + public int Id + { + get { return IdManager.GetId(ref _id); } + } - /// - /// Sends a signal to a single task waiting on this condition variable. The associated lock MUST be held when calling this method, and it will still be held when this method returns. - /// - public void Notify() + /// + /// Sends a signal to a single task waiting on this condition variable. The associated lock MUST be held when calling this method, and it will still be held when this method returns. + /// + public void Notify() + { + lock (_mutex) { - lock (_mutex) - { - if (!_queue.IsEmpty) - _queue.Dequeue(); - } + if (!_queue.IsEmpty) + _queue.Dequeue(); } + } - /// - /// Sends a signal to all tasks waiting on this condition variable. The associated lock MUST be held when calling this method, and it will still be held when this method returns. - /// - public void NotifyAll() + /// + /// Sends a signal to all tasks waiting on this condition variable. The associated lock MUST be held when calling this method, and it will still be held when this method returns. + /// + public void NotifyAll() + { + lock (_mutex) { - lock (_mutex) - { - _queue.DequeueAll(); - } + _queue.DequeueAll(); } + } - /// - /// Asynchronously waits for a signal on this condition variable. The associated lock MUST be held when calling this method, and it will still be held when this method returns, even if the method is cancelled. - /// - /// The cancellation signal used to cancel this wait. - public Task WaitAsync(CancellationToken cancellationToken) + /// + /// Asynchronously waits for a signal on this condition variable. The associated lock MUST be held when calling this method, and it will still be held when this method returns, even if the method is cancelled. + /// + /// The cancellation signal used to cancel this wait. + public Task WaitAsync(CancellationToken cancellationToken) + { + Task task; + lock (_mutex) { - Task task; - lock (_mutex) - { - // Begin waiting for either a signal or cancellation. - task = _queue.Enqueue(_mutex, cancellationToken); + // Begin waiting for either a signal or cancellation. + task = _queue.Enqueue(_mutex, cancellationToken); - // Attach to the signal or cancellation. - var ret = WaitAndRetakeLockAsync(task, _asyncLock); + // Attach to the signal or cancellation. + var ret = WaitAndRetakeLockAsync(task, _asyncLock); - // Release the lock while we are waiting. - _asyncLock.ReleaseLock(); + // Release the lock while we are waiting. + _asyncLock.ReleaseLock(); - return ret; - } + return ret; } + } - private static async Task WaitAndRetakeLockAsync(Task task, AsyncLock asyncLock) + private static async Task WaitAndRetakeLockAsync(Task task, AsyncLock asyncLock) + { + try { - try - { - await task.ConfigureAwait(false); - } - finally - { - // Re-take the lock. - await asyncLock.LockAsync().ConfigureAwait(false); - } + await task.ConfigureAwait(false); } - - /// - /// Asynchronously waits for a signal on this condition variable. The associated lock MUST be held when calling this method, and it will still be held when the returned task completes. - /// - public Task WaitAsync() + finally { - return WaitAsync(CancellationToken.None); + // Re-take the lock. + await asyncLock.LockAsync().ConfigureAwait(false); } + } - /// - /// Synchronously waits for a signal on this condition variable. This method may block the calling thread. The associated lock MUST be held when calling this method, and it will still be held when this method returns, even if the method is cancelled. - /// - /// The cancellation signal used to cancel this wait. - public void Wait(CancellationToken cancellationToken) - { - WaitAsync(cancellationToken).WaitAndUnwrapException(); - } + /// + /// Asynchronously waits for a signal on this condition variable. The associated lock MUST be held when calling this method, and it will still be held when the returned task completes. + /// + public Task WaitAsync() + { + return WaitAsync(CancellationToken.None); + } - /// - /// Synchronously waits for a signal on this condition variable. This method may block the calling thread. The associated lock MUST be held when calling this method, and it will still be held when this method returns. - /// - public void Wait() - { - Wait(CancellationToken.None); - } + /// + /// Synchronously waits for a signal on this condition variable. This method may block the calling thread. The associated lock MUST be held when calling this method, and it will still be held when this method returns, even if the method is cancelled. + /// + /// The cancellation signal used to cancel this wait. + public void Wait(CancellationToken cancellationToken) + { + WaitAsync(cancellationToken).WaitAndUnwrapException(); + } - // ReSharper disable UnusedMember.Local - [DebuggerNonUserCode] - private sealed class DebugView - { - private readonly AsyncConditionVariable _cv; + /// + /// Synchronously waits for a signal on this condition variable. This method may block the calling thread. The associated lock MUST be held when calling this method, and it will still be held when this method returns. + /// + public void Wait() + { + Wait(CancellationToken.None); + } + + // ReSharper disable UnusedMember.Local + [DebuggerNonUserCode] + private sealed class DebugView + { + private readonly AsyncConditionVariable _cv; - public DebugView(AsyncConditionVariable cv) - { - _cv = cv; - } + public DebugView(AsyncConditionVariable cv) + { + _cv = cv; + } - public int Id { get { return _cv.Id; } } + public int Id { get { return _cv.Id; } } - public AsyncLock AsyncLock { get { return _cv._asyncLock; } } + public AsyncLock AsyncLock { get { return _cv._asyncLock; } } - public IAsyncWaitQueue WaitQueue { get { return _cv._queue; } } - } - // ReSharper restore UnusedMember.Local + public IAsyncWaitQueue WaitQueue { get { return _cv._queue; } } } + // ReSharper restore UnusedMember.Local } diff --git a/src/Foundatio/Nito.AsyncEx.Coordination/AsyncCountdownEvent.cs b/src/Foundatio/Nito.AsyncEx.Coordination/AsyncCountdownEvent.cs index 0b2893427..744cee454 100644 --- a/src/Foundatio/Nito.AsyncEx.Coordination/AsyncCountdownEvent.cs +++ b/src/Foundatio/Nito.AsyncEx.Coordination/AsyncCountdownEvent.cs @@ -5,175 +5,174 @@ // Original idea by Stephen Toub: http://blogs.msdn.com/b/pfxteam/archive/2012/02/11/10266930.aspx -namespace Foundatio.AsyncEx +namespace Foundatio.AsyncEx; + +/// +/// An async-compatible countdown event. +/// +[DebuggerDisplay("Id = {Id}, CurrentCount = {_count}")] +[DebuggerTypeProxy(typeof(DebugView))] +public sealed class AsyncCountdownEvent { /// - /// An async-compatible countdown event. + /// The underlying manual-reset event. /// - [DebuggerDisplay("Id = {Id}, CurrentCount = {_count}")] - [DebuggerTypeProxy(typeof(DebugView))] - public sealed class AsyncCountdownEvent + private readonly AsyncManualResetEvent _mre; + + /// + /// The remaining count on this event. + /// + private long _count; + + /// + /// Creates an async-compatible countdown event. + /// + /// The number of signals this event will need before it becomes set. + public AsyncCountdownEvent(long count) { - /// - /// The underlying manual-reset event. - /// - private readonly AsyncManualResetEvent _mre; - - /// - /// The remaining count on this event. - /// - private long _count; - - /// - /// Creates an async-compatible countdown event. - /// - /// The number of signals this event will need before it becomes set. - public AsyncCountdownEvent(long count) - { - _mre = new AsyncManualResetEvent(count == 0); - _count = count; - } + _mre = new AsyncManualResetEvent(count == 0); + _count = count; + } - /// - /// Gets a semi-unique identifier for this asynchronous countdown event. - /// - public int Id - { - get { return _mre.Id; } - } + /// + /// Gets a semi-unique identifier for this asynchronous countdown event. + /// + public int Id + { + get { return _mre.Id; } + } - /// - /// Gets the current number of remaining signals before this event becomes set. This member is seldom used; code using this member has a high possibility of race conditions. - /// - public long CurrentCount + /// + /// Gets the current number of remaining signals before this event becomes set. This member is seldom used; code using this member has a high possibility of race conditions. + /// + public long CurrentCount + { + get { - get - { - lock (_mre) - return _count; - } + lock (_mre) + return _count; } + } - /// - /// Asynchronously waits for the count to reach zero. - /// - public Task WaitAsync() - { - return _mre.WaitAsync(); - } + /// + /// Asynchronously waits for the count to reach zero. + /// + public Task WaitAsync() + { + return _mre.WaitAsync(); + } - /// - /// Synchronously waits for the count to reach zero. This method may block the calling thread. - /// - /// The cancellation token used to cancel the wait. If this token is already canceled, this method will first check whether the event is set. - public Task WaitAsync(CancellationToken cancellationToken) - { - return _mre.WaitAsync(cancellationToken); - } + /// + /// Synchronously waits for the count to reach zero. This method may block the calling thread. + /// + /// The cancellation token used to cancel the wait. If this token is already canceled, this method will first check whether the event is set. + public Task WaitAsync(CancellationToken cancellationToken) + { + return _mre.WaitAsync(cancellationToken); + } - /// - /// Synchronously waits for the count to reach zero. This method may block the calling thread. - /// - public void Wait() - { - _mre.Wait(); - } + /// + /// Synchronously waits for the count to reach zero. This method may block the calling thread. + /// + public void Wait() + { + _mre.Wait(); + } - /// - /// Synchronously waits for the count to reach zero. This method may block the calling thread. - /// - /// The cancellation token used to cancel the wait. If this token is already canceled, this method will first check whether the event is set. - public void Wait(CancellationToken cancellationToken) - { - _mre.Wait(cancellationToken); - } + /// + /// Synchronously waits for the count to reach zero. This method may block the calling thread. + /// + /// The cancellation token used to cancel the wait. If this token is already canceled, this method will first check whether the event is set. + public void Wait(CancellationToken cancellationToken) + { + _mre.Wait(cancellationToken); + } - /// - /// Attempts to modify the current count by the specified amount. - /// - /// The amount to change the current count. - /// true to add to the current count; false to subtract. - private void ModifyCount(long difference, bool add) + /// + /// Attempts to modify the current count by the specified amount. + /// + /// The amount to change the current count. + /// true to add to the current count; false to subtract. + private void ModifyCount(long difference, bool add) + { + if (difference == 0) + return; + lock (_mre) { - if (difference == 0) - return; - lock (_mre) + var oldCount = _count; + checked + { + if (add) + _count += difference; + else + _count -= difference; + } + if (oldCount == 0) + { + _mre.Reset(); + } + else if (_count == 0) + { + _mre.Set(); + } + else if ((oldCount < 0 && _count > 0) || (oldCount > 0 && _count < 0)) { - var oldCount = _count; - checked - { - if (add) - _count += difference; - else - _count -= difference; - } - if (oldCount == 0) - { - _mre.Reset(); - } - else if (_count == 0) - { - _mre.Set(); - } - else if ((oldCount < 0 && _count > 0) || (oldCount > 0 && _count < 0)) - { - _mre.Set(); - _mre.Reset(); - } + _mre.Set(); + _mre.Reset(); } } + } - /// - /// Adds the specified value to the current count. - /// - /// The amount to change the current count. - public void AddCount(long addCount) - { - ModifyCount(addCount, true); - } + /// + /// Adds the specified value to the current count. + /// + /// The amount to change the current count. + public void AddCount(long addCount) + { + ModifyCount(addCount, true); + } - /// - /// Adds one to the current count. - /// - public void AddCount() - { - AddCount(1); - } + /// + /// Adds one to the current count. + /// + public void AddCount() + { + AddCount(1); + } - /// - /// Subtracts the specified value from the current count. - /// - /// The amount to change the current count. - public void Signal(long signalCount) - { - ModifyCount(signalCount, false); - } + /// + /// Subtracts the specified value from the current count. + /// + /// The amount to change the current count. + public void Signal(long signalCount) + { + ModifyCount(signalCount, false); + } - /// - /// Subtracts one from the current count. - /// - public void Signal() - { - Signal(1); - } + /// + /// Subtracts one from the current count. + /// + public void Signal() + { + Signal(1); + } - // ReSharper disable UnusedMember.Local - [DebuggerNonUserCode] - private sealed class DebugView - { - private readonly AsyncCountdownEvent _ce; + // ReSharper disable UnusedMember.Local + [DebuggerNonUserCode] + private sealed class DebugView + { + private readonly AsyncCountdownEvent _ce; - public DebugView(AsyncCountdownEvent ce) - { - _ce = ce; - } + public DebugView(AsyncCountdownEvent ce) + { + _ce = ce; + } - public int Id { get { return _ce.Id; } } + public int Id { get { return _ce.Id; } } - public long CurrentCount { get { return _ce.CurrentCount; } } + public long CurrentCount { get { return _ce.CurrentCount; } } - public AsyncManualResetEvent AsyncManualResetEvent { get { return _ce._mre; } } - } - // ReSharper restore UnusedMember.Local + public AsyncManualResetEvent AsyncManualResetEvent { get { return _ce._mre; } } } + // ReSharper restore UnusedMember.Local } diff --git a/src/Foundatio/Nito.AsyncEx.Coordination/AsyncLazy.cs b/src/Foundatio/Nito.AsyncEx.Coordination/AsyncLazy.cs index 78f393aa6..ef0cd0b0c 100644 --- a/src/Foundatio/Nito.AsyncEx.Coordination/AsyncLazy.cs +++ b/src/Foundatio/Nito.AsyncEx.Coordination/AsyncLazy.cs @@ -3,211 +3,210 @@ using System.Runtime.CompilerServices; using System.Threading.Tasks; -namespace Foundatio.AsyncEx +namespace Foundatio.AsyncEx; + +/// +/// Flags controlling the behavior of . +/// +[Flags] +public enum AsyncLazyFlags { /// - /// Flags controlling the behavior of . + /// No special flags. The factory method is executed on a thread pool thread, and does not retry initialization on failures (failures are cached). /// - [Flags] - public enum AsyncLazyFlags - { - /// - /// No special flags. The factory method is executed on a thread pool thread, and does not retry initialization on failures (failures are cached). - /// - None = 0x0, - - /// - /// Execute the factory method on the calling thread. - /// - ExecuteOnCallingThread = 0x1, - - /// - /// If the factory method fails, then re-run the factory method the next time instead of caching the failed task. - /// - RetryOnFailure = 0x2, - } + None = 0x0, + + /// + /// Execute the factory method on the calling thread. + /// + ExecuteOnCallingThread = 0x1, + + /// + /// If the factory method fails, then re-run the factory method the next time instead of caching the failed task. + /// + RetryOnFailure = 0x2, +} + +/// +/// Provides support for asynchronous lazy initialization. This type is fully threadsafe. +/// +/// The type of object that is being asynchronously initialized. +[DebuggerDisplay("Id = {Id}, State = {GetStateForDebugger}")] +[DebuggerTypeProxy(typeof(AsyncLazy<>.DebugView))] +public sealed class AsyncLazy +{ + /// + /// The synchronization object protecting _instance. + /// + private readonly object _mutex; + + /// + /// The factory method to call. + /// + private readonly Func> _factory; + + /// + /// The underlying lazy task. + /// + private Lazy> _instance; /// - /// Provides support for asynchronous lazy initialization. This type is fully threadsafe. + /// The semi-unique identifier for this instance. This is 0 if the id has not yet been created. /// - /// The type of object that is being asynchronously initialized. - [DebuggerDisplay("Id = {Id}, State = {GetStateForDebugger}")] - [DebuggerTypeProxy(typeof(AsyncLazy<>.DebugView))] - public sealed class AsyncLazy + private int _id; + + [DebuggerNonUserCode] + internal LazyState GetStateForDebugger { - /// - /// The synchronization object protecting _instance. - /// - private readonly object _mutex; - - /// - /// The factory method to call. - /// - private readonly Func> _factory; - - /// - /// The underlying lazy task. - /// - private Lazy> _instance; - - /// - /// The semi-unique identifier for this instance. This is 0 if the id has not yet been created. - /// - private int _id; - - [DebuggerNonUserCode] - internal LazyState GetStateForDebugger + get { - get - { - if (!_instance.IsValueCreated) - return LazyState.NotStarted; - if (!_instance.Value.IsCompleted) - return LazyState.Executing; - return LazyState.Completed; - } + if (!_instance.IsValueCreated) + return LazyState.NotStarted; + if (!_instance.Value.IsCompleted) + return LazyState.Executing; + return LazyState.Completed; } + } - /// - /// Initializes a new instance of the class. - /// - /// The asynchronous delegate that is invoked to produce the value when it is needed. May not be null. - /// Flags to influence async lazy semantics. - public AsyncLazy(Func> factory, AsyncLazyFlags flags = AsyncLazyFlags.None) - { - if (factory == null) - throw new ArgumentNullException(nameof(factory)); - _factory = factory; - if ((flags & AsyncLazyFlags.RetryOnFailure) == AsyncLazyFlags.RetryOnFailure) - _factory = RetryOnFailure(_factory); - if ((flags & AsyncLazyFlags.ExecuteOnCallingThread) != AsyncLazyFlags.ExecuteOnCallingThread) - _factory = RunOnThreadPool(_factory); - - _mutex = new object(); - _instance = new Lazy>(_factory); - } + /// + /// Initializes a new instance of the class. + /// + /// The asynchronous delegate that is invoked to produce the value when it is needed. May not be null. + /// Flags to influence async lazy semantics. + public AsyncLazy(Func> factory, AsyncLazyFlags flags = AsyncLazyFlags.None) + { + if (factory == null) + throw new ArgumentNullException(nameof(factory)); + _factory = factory; + if ((flags & AsyncLazyFlags.RetryOnFailure) == AsyncLazyFlags.RetryOnFailure) + _factory = RetryOnFailure(_factory); + if ((flags & AsyncLazyFlags.ExecuteOnCallingThread) != AsyncLazyFlags.ExecuteOnCallingThread) + _factory = RunOnThreadPool(_factory); + + _mutex = new object(); + _instance = new Lazy>(_factory); + } - /// - /// Gets a semi-unique identifier for this asynchronous lazy instance. - /// - public int Id + /// + /// Gets a semi-unique identifier for this asynchronous lazy instance. + /// + public int Id + { + get { return IdManager>.GetId(ref _id); } + } + + /// + /// Whether the asynchronous factory method has started. This is initially false and becomes true when this instance is awaited or after is called. + /// + public bool IsStarted + { + get { - get { return IdManager>.GetId(ref _id); } + lock (_mutex) + return _instance.IsValueCreated; } + } - /// - /// Whether the asynchronous factory method has started. This is initially false and becomes true when this instance is awaited or after is called. - /// - public bool IsStarted + /// + /// Starts the asynchronous factory method, if it has not already started, and returns the resulting task. + /// + public Task Task + { + get { - get - { - lock (_mutex) - return _instance.IsValueCreated; - } + lock (_mutex) + return _instance.Value; } + } - /// - /// Starts the asynchronous factory method, if it has not already started, and returns the resulting task. - /// - public Task Task + private Func> RetryOnFailure(Func> factory) + { + return async () => { - get + try { - lock (_mutex) - return _instance.Value; + return await factory().ConfigureAwait(false); } - } - - private Func> RetryOnFailure(Func> factory) - { - return async () => + catch { - try - { - return await factory().ConfigureAwait(false); - } - catch + lock (_mutex) { - lock (_mutex) - { - _instance = new Lazy>(_factory); - } - throw; + _instance = new Lazy>(_factory); } - }; - } + throw; + } + }; + } - private Func> RunOnThreadPool(Func> factory) - { - return () => System.Threading.Tasks.Task.Run(factory); - } + private Func> RunOnThreadPool(Func> factory) + { + return () => System.Threading.Tasks.Task.Run(factory); + } - /// - /// Asynchronous infrastructure support. This method permits instances of to be await'ed. - /// - [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] - public TaskAwaiter GetAwaiter() - { - return Task.GetAwaiter(); - } + /// + /// Asynchronous infrastructure support. This method permits instances of to be await'ed. + /// + [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] + public TaskAwaiter GetAwaiter() + { + return Task.GetAwaiter(); + } - /// - /// Asynchronous infrastructure support. This method permits instances of to be await'ed. - /// - [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] - public ConfiguredTaskAwaitable ConfigureAwait(bool continueOnCapturedContext) - { - return Task.ConfigureAwait(continueOnCapturedContext); - } + /// + /// Asynchronous infrastructure support. This method permits instances of to be await'ed. + /// + [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] + public ConfiguredTaskAwaitable ConfigureAwait(bool continueOnCapturedContext) + { + return Task.ConfigureAwait(continueOnCapturedContext); + } - /// - /// Starts the asynchronous initialization, if it has not already started. - /// - public void Start() - { - // ReSharper disable UnusedVariable - var unused = Task; - // ReSharper restore UnusedVariable - } + /// + /// Starts the asynchronous initialization, if it has not already started. + /// + public void Start() + { + // ReSharper disable UnusedVariable + var unused = Task; + // ReSharper restore UnusedVariable + } - internal enum LazyState - { - NotStarted, - Executing, - Completed - } + internal enum LazyState + { + NotStarted, + Executing, + Completed + } - [DebuggerNonUserCode] - internal sealed class DebugView - { - private readonly AsyncLazy _lazy; + [DebuggerNonUserCode] + internal sealed class DebugView + { + private readonly AsyncLazy _lazy; - public DebugView(AsyncLazy lazy) - { - _lazy = lazy; - } + public DebugView(AsyncLazy lazy) + { + _lazy = lazy; + } - public LazyState State { get { return _lazy.GetStateForDebugger; } } + public LazyState State { get { return _lazy.GetStateForDebugger; } } - public Task Task + public Task Task + { + get { - get - { - if (!_lazy._instance.IsValueCreated) - throw new InvalidOperationException("Not yet created"); - return _lazy._instance.Value; - } + if (!_lazy._instance.IsValueCreated) + throw new InvalidOperationException("Not yet created"); + return _lazy._instance.Value; } + } - public T Value + public T Value + { + get { - get - { - if (!_lazy._instance.IsValueCreated || !_lazy._instance.Value.IsCompleted) - throw new InvalidOperationException("Not yet created"); - return _lazy._instance.Value.Result; - } + if (!_lazy._instance.IsValueCreated || !_lazy._instance.Value.IsCompleted) + throw new InvalidOperationException("Not yet created"); + return _lazy._instance.Value.Result; } } } diff --git a/src/Foundatio/Nito.AsyncEx.Coordination/AsyncLock.cs b/src/Foundatio/Nito.AsyncEx.Coordination/AsyncLock.cs index 210cf30c0..dac1be037 100644 --- a/src/Foundatio/Nito.AsyncEx.Coordination/AsyncLock.cs +++ b/src/Foundatio/Nito.AsyncEx.Coordination/AsyncLock.cs @@ -6,202 +6,201 @@ // Original idea from Stephen Toub: http://blogs.msdn.com/b/pfxteam/archive/2012/02/12/10266988.aspx -namespace Foundatio.AsyncEx +namespace Foundatio.AsyncEx; + +/// +/// A mutual exclusion lock that is compatible with async. Note that this lock is not recursive! +/// +/// +/// This is the async-ready almost-equivalent of the lock keyword or the type, similar to Stephen Toub's AsyncLock. It's only almost equivalent because the lock keyword permits reentrancy, which is not currently possible to do with an async-ready lock. +/// An is either taken or not. The lock can be asynchronously acquired by calling , and it is released by disposing the result of that task. takes an optional , which can be used to cancel the acquiring of the lock. +/// The task returned from will enter the Completed state when it has acquired the . That same task will enter the Canceled state if the is signaled before the wait is satisfied; in that case, the is not taken by that task. +/// You can call or with an already-cancelled to attempt to acquire the immediately without actually entering the wait queue. +/// +/// +/// The vast majority of use cases are to just replace a lock statement. That is, with the original code looking like this: +/// +/// private readonly object _mutex = new object(); +/// public void DoStuff() +/// { +/// lock (_mutex) +/// { +/// Thread.Sleep(TimeSpan.FromSeconds(1)); +/// } +/// } +/// +/// If we want to replace the blocking operation Thread.Sleep with an asynchronous equivalent, it's not directly possible because of the lock block. We cannot await inside of a lock. +/// So, we use the async-compatible instead: +/// +/// private readonly AsyncLock _mutex = new AsyncLock(); +/// public async Task DoStuffAsync() +/// { +/// using (await _mutex.LockAsync()) +/// { +/// await Task.Delay(TimeSpan.FromSeconds(1)); +/// } +/// } +/// +/// +[DebuggerDisplay("Id = {Id}, Taken = {_taken}")] +[DebuggerTypeProxy(typeof(DebugView))] +public sealed class AsyncLock { /// - /// A mutual exclusion lock that is compatible with async. Note that this lock is not recursive! + /// Whether the lock is taken by a task. /// - /// - /// This is the async-ready almost-equivalent of the lock keyword or the type, similar to Stephen Toub's AsyncLock. It's only almost equivalent because the lock keyword permits reentrancy, which is not currently possible to do with an async-ready lock. - /// An is either taken or not. The lock can be asynchronously acquired by calling , and it is released by disposing the result of that task. takes an optional , which can be used to cancel the acquiring of the lock. - /// The task returned from will enter the Completed state when it has acquired the . That same task will enter the Canceled state if the is signaled before the wait is satisfied; in that case, the is not taken by that task. - /// You can call or with an already-cancelled to attempt to acquire the immediately without actually entering the wait queue. - /// - /// - /// The vast majority of use cases are to just replace a lock statement. That is, with the original code looking like this: - /// - /// private readonly object _mutex = new object(); - /// public void DoStuff() - /// { - /// lock (_mutex) - /// { - /// Thread.Sleep(TimeSpan.FromSeconds(1)); - /// } - /// } - /// - /// If we want to replace the blocking operation Thread.Sleep with an asynchronous equivalent, it's not directly possible because of the lock block. We cannot await inside of a lock. - /// So, we use the async-compatible instead: - /// - /// private readonly AsyncLock _mutex = new AsyncLock(); - /// public async Task DoStuffAsync() - /// { - /// using (await _mutex.LockAsync()) - /// { - /// await Task.Delay(TimeSpan.FromSeconds(1)); - /// } - /// } - /// - /// - [DebuggerDisplay("Id = {Id}, Taken = {_taken}")] - [DebuggerTypeProxy(typeof(DebugView))] - public sealed class AsyncLock - { - /// - /// Whether the lock is taken by a task. - /// - private bool _taken; + private bool _taken; - /// - /// The queue of TCSs that other tasks are awaiting to acquire the lock. - /// - private readonly IAsyncWaitQueue _queue; + /// + /// The queue of TCSs that other tasks are awaiting to acquire the lock. + /// + private readonly IAsyncWaitQueue _queue; - /// - /// The semi-unique identifier for this instance. This is 0 if the id has not yet been created. - /// - private int _id; + /// + /// The semi-unique identifier for this instance. This is 0 if the id has not yet been created. + /// + private int _id; - /// - /// The object used for mutual exclusion. - /// - private readonly object _mutex; + /// + /// The object used for mutual exclusion. + /// + private readonly object _mutex; - /// - /// Creates a new async-compatible mutual exclusion lock. - /// - public AsyncLock() - : this(null) - { - } + /// + /// Creates a new async-compatible mutual exclusion lock. + /// + public AsyncLock() + : this(null) + { + } - /// - /// Creates a new async-compatible mutual exclusion lock using the specified wait queue. - /// - /// The wait queue used to manage waiters. This may be null to use a default (FIFO) queue. - internal AsyncLock(IAsyncWaitQueue queue) - { - _queue = queue ?? new DefaultAsyncWaitQueue(); - _mutex = new object(); - } + /// + /// Creates a new async-compatible mutual exclusion lock using the specified wait queue. + /// + /// The wait queue used to manage waiters. This may be null to use a default (FIFO) queue. + internal AsyncLock(IAsyncWaitQueue queue) + { + _queue = queue ?? new DefaultAsyncWaitQueue(); + _mutex = new object(); + } - /// - /// Gets a semi-unique identifier for this asynchronous lock. - /// - public int Id - { - get { return IdManager.GetId(ref _id); } - } + /// + /// Gets a semi-unique identifier for this asynchronous lock. + /// + public int Id + { + get { return IdManager.GetId(ref _id); } + } - /// - /// Asynchronously acquires the lock. Returns a disposable that releases the lock when disposed. - /// - /// The cancellation token used to cancel the lock. If this is already set, then this method will attempt to take the lock immediately (succeeding if the lock is currently available). - /// A disposable that releases the lock when disposed. - private Task RequestLockAsync(CancellationToken cancellationToken) + /// + /// Asynchronously acquires the lock. Returns a disposable that releases the lock when disposed. + /// + /// The cancellation token used to cancel the lock. If this is already set, then this method will attempt to take the lock immediately (succeeding if the lock is currently available). + /// A disposable that releases the lock when disposed. + private Task RequestLockAsync(CancellationToken cancellationToken) + { + lock (_mutex) { - lock (_mutex) + if (!_taken) + { + // If the lock is available, take it immediately. + _taken = true; + return Task.FromResult(new Key(this)); + } + else { - if (!_taken) - { - // If the lock is available, take it immediately. - _taken = true; - return Task.FromResult(new Key(this)); - } - else - { - // Wait for the lock to become available or cancellation. - return _queue.Enqueue(_mutex, cancellationToken); - } + // Wait for the lock to become available or cancellation. + return _queue.Enqueue(_mutex, cancellationToken); } } + } - /// - /// Asynchronously acquires the lock. Returns a disposable that releases the lock when disposed. - /// - /// The cancellation token used to cancel the lock. If this is already set, then this method will attempt to take the lock immediately (succeeding if the lock is currently available). - /// A disposable that releases the lock when disposed. - public AwaitableDisposable LockAsync(CancellationToken cancellationToken) - { - return new AwaitableDisposable(RequestLockAsync(cancellationToken)); - } + /// + /// Asynchronously acquires the lock. Returns a disposable that releases the lock when disposed. + /// + /// The cancellation token used to cancel the lock. If this is already set, then this method will attempt to take the lock immediately (succeeding if the lock is currently available). + /// A disposable that releases the lock when disposed. + public AwaitableDisposable LockAsync(CancellationToken cancellationToken) + { + return new AwaitableDisposable(RequestLockAsync(cancellationToken)); + } - /// - /// Asynchronously acquires the lock. Returns a disposable that releases the lock when disposed. - /// - /// A disposable that releases the lock when disposed. - public AwaitableDisposable LockAsync() - { - return LockAsync(CancellationToken.None); - } + /// + /// Asynchronously acquires the lock. Returns a disposable that releases the lock when disposed. + /// + /// A disposable that releases the lock when disposed. + public AwaitableDisposable LockAsync() + { + return LockAsync(CancellationToken.None); + } - /// - /// Synchronously acquires the lock. Returns a disposable that releases the lock when disposed. This method may block the calling thread. - /// - /// The cancellation token used to cancel the lock. If this is already set, then this method will attempt to take the lock immediately (succeeding if the lock is currently available). - public IDisposable Lock(CancellationToken cancellationToken) - { - return RequestLockAsync(cancellationToken).WaitAndUnwrapException(); - } + /// + /// Synchronously acquires the lock. Returns a disposable that releases the lock when disposed. This method may block the calling thread. + /// + /// The cancellation token used to cancel the lock. If this is already set, then this method will attempt to take the lock immediately (succeeding if the lock is currently available). + public IDisposable Lock(CancellationToken cancellationToken) + { + return RequestLockAsync(cancellationToken).WaitAndUnwrapException(); + } - /// - /// Synchronously acquires the lock. Returns a disposable that releases the lock when disposed. This method may block the calling thread. - /// - public IDisposable Lock() + /// + /// Synchronously acquires the lock. Returns a disposable that releases the lock when disposed. This method may block the calling thread. + /// + public IDisposable Lock() + { + return Lock(CancellationToken.None); + } + + /// + /// Releases the lock. + /// + internal void ReleaseLock() + { + lock (_mutex) { - return Lock(CancellationToken.None); + if (_queue.IsEmpty) + _taken = false; + else + _queue.Dequeue(new Key(this)); } + } + /// + /// The disposable which releases the lock. + /// + private sealed class Key : Disposables.SingleDisposable + { /// - /// Releases the lock. + /// Creates the key for a lock. /// - internal void ReleaseLock() + /// The lock to release. May not be null. + public Key(AsyncLock asyncLock) + : base(asyncLock) { - lock (_mutex) - { - if (_queue.IsEmpty) - _taken = false; - else - _queue.Dequeue(new Key(this)); - } } - /// - /// The disposable which releases the lock. - /// - private sealed class Key : Disposables.SingleDisposable + protected override void Dispose(AsyncLock context) { - /// - /// Creates the key for a lock. - /// - /// The lock to release. May not be null. - public Key(AsyncLock asyncLock) - : base(asyncLock) - { - } - - protected override void Dispose(AsyncLock context) - { - context.ReleaseLock(); - } + context.ReleaseLock(); } + } - // ReSharper disable UnusedMember.Local - [DebuggerNonUserCode] - private sealed class DebugView - { - private readonly AsyncLock _mutex; + // ReSharper disable UnusedMember.Local + [DebuggerNonUserCode] + private sealed class DebugView + { + private readonly AsyncLock _mutex; - public DebugView(AsyncLock mutex) - { - _mutex = mutex; - } + public DebugView(AsyncLock mutex) + { + _mutex = mutex; + } - public int Id { get { return _mutex.Id; } } + public int Id { get { return _mutex.Id; } } - public bool Taken { get { return _mutex._taken; } } + public bool Taken { get { return _mutex._taken; } } - public IAsyncWaitQueue WaitQueue { get { return _mutex._queue; } } - } - // ReSharper restore UnusedMember.Local + public IAsyncWaitQueue WaitQueue { get { return _mutex._queue; } } } + // ReSharper restore UnusedMember.Local } diff --git a/src/Foundatio/Nito.AsyncEx.Coordination/AsyncManualResetEvent.cs b/src/Foundatio/Nito.AsyncEx.Coordination/AsyncManualResetEvent.cs index 758004234..a01c7c0e1 100644 --- a/src/Foundatio/Nito.AsyncEx.Coordination/AsyncManualResetEvent.cs +++ b/src/Foundatio/Nito.AsyncEx.Coordination/AsyncManualResetEvent.cs @@ -5,158 +5,157 @@ // Original idea by Stephen Toub: http://blogs.msdn.com/b/pfxteam/archive/2012/02/11/10266920.aspx -namespace Foundatio.AsyncEx +namespace Foundatio.AsyncEx; + +/// +/// An async-compatible manual-reset event. +/// +[DebuggerDisplay("Id = {Id}, IsSet = {GetStateForDebugger}")] +[DebuggerTypeProxy(typeof(DebugView))] +public sealed class AsyncManualResetEvent { /// - /// An async-compatible manual-reset event. + /// The object used for synchronization. /// - [DebuggerDisplay("Id = {Id}, IsSet = {GetStateForDebugger}")] - [DebuggerTypeProxy(typeof(DebugView))] - public sealed class AsyncManualResetEvent + private readonly object _mutex; + + /// + /// The current state of the event. + /// + private TaskCompletionSource _tcs; + + /// + /// The semi-unique identifier for this instance. This is 0 if the id has not yet been created. + /// + private int _id; + + [DebuggerNonUserCode] + private bool GetStateForDebugger { - /// - /// The object used for synchronization. - /// - private readonly object _mutex; - - /// - /// The current state of the event. - /// - private TaskCompletionSource _tcs; - - /// - /// The semi-unique identifier for this instance. This is 0 if the id has not yet been created. - /// - private int _id; - - [DebuggerNonUserCode] - private bool GetStateForDebugger + get { - get - { - return _tcs.Task.IsCompleted; - } + return _tcs.Task.IsCompleted; } + } - /// - /// Creates an async-compatible manual-reset event. - /// - /// Whether the manual-reset event is initially set or unset. - public AsyncManualResetEvent(bool set) - { - _mutex = new object(); - _tcs = TaskCompletionSourceExtensions.CreateAsyncTaskSource(); - if (set) - _tcs.TrySetResult(null); - } + /// + /// Creates an async-compatible manual-reset event. + /// + /// Whether the manual-reset event is initially set or unset. + public AsyncManualResetEvent(bool set) + { + _mutex = new object(); + _tcs = TaskCompletionSourceExtensions.CreateAsyncTaskSource(); + if (set) + _tcs.TrySetResult(null); + } - /// - /// Creates an async-compatible manual-reset event that is initially unset. - /// - public AsyncManualResetEvent() - : this(false) - { - } + /// + /// Creates an async-compatible manual-reset event that is initially unset. + /// + public AsyncManualResetEvent() + : this(false) + { + } - /// - /// Gets a semi-unique identifier for this asynchronous manual-reset event. - /// - public int Id - { - get { return IdManager.GetId(ref _id); } - } + /// + /// Gets a semi-unique identifier for this asynchronous manual-reset event. + /// + public int Id + { + get { return IdManager.GetId(ref _id); } + } - /// - /// Whether this event is currently set. This member is seldom used; code using this member has a high possibility of race conditions. - /// - public bool IsSet - { - get { lock (_mutex) return _tcs.Task.IsCompleted; } - } + /// + /// Whether this event is currently set. This member is seldom used; code using this member has a high possibility of race conditions. + /// + public bool IsSet + { + get { lock (_mutex) return _tcs.Task.IsCompleted; } + } - /// - /// Asynchronously waits for this event to be set. - /// - public Task WaitAsync() + /// + /// Asynchronously waits for this event to be set. + /// + public Task WaitAsync() + { + lock (_mutex) { - lock (_mutex) - { - return _tcs.Task; - } + return _tcs.Task; } + } - /// - /// Asynchronously waits for this event to be set or for the wait to be canceled. - /// - /// The cancellation token used to cancel the wait. If this token is already canceled, this method will first check whether the event is set. - public Task WaitAsync(CancellationToken cancellationToken) - { - var waitTask = WaitAsync(); - if (waitTask.IsCompleted) - return waitTask; - return waitTask.WaitAsync(cancellationToken); - } + /// + /// Asynchronously waits for this event to be set or for the wait to be canceled. + /// + /// The cancellation token used to cancel the wait. If this token is already canceled, this method will first check whether the event is set. + public Task WaitAsync(CancellationToken cancellationToken) + { + var waitTask = WaitAsync(); + if (waitTask.IsCompleted) + return waitTask; + return waitTask.WaitAsync(cancellationToken); + } - /// - /// Synchronously waits for this event to be set. This method may block the calling thread. - /// - public void Wait() - { - WaitAsync().WaitAndUnwrapException(); - } + /// + /// Synchronously waits for this event to be set. This method may block the calling thread. + /// + public void Wait() + { + WaitAsync().WaitAndUnwrapException(); + } - /// - /// Synchronously waits for this event to be set. This method may block the calling thread. - /// - /// The cancellation token used to cancel the wait. If this token is already canceled, this method will first check whether the event is set. - public void Wait(CancellationToken cancellationToken) - { - var ret = WaitAsync(); - if (ret.IsCompleted) - return; - ret.WaitAndUnwrapException(cancellationToken); - } + /// + /// Synchronously waits for this event to be set. This method may block the calling thread. + /// + /// The cancellation token used to cancel the wait. If this token is already canceled, this method will first check whether the event is set. + public void Wait(CancellationToken cancellationToken) + { + var ret = WaitAsync(); + if (ret.IsCompleted) + return; + ret.WaitAndUnwrapException(cancellationToken); + } - /// - /// Sets the event, atomically completing every task returned by . If the event is already set, this method does nothing. - /// - public void Set() + /// + /// Sets the event, atomically completing every task returned by . If the event is already set, this method does nothing. + /// + public void Set() + { + lock (_mutex) { - lock (_mutex) - { - _tcs.TrySetResult(null); - } + _tcs.TrySetResult(null); } + } - /// - /// Resets the event. If the event is already reset, this method does nothing. - /// - public void Reset() + /// + /// Resets the event. If the event is already reset, this method does nothing. + /// + public void Reset() + { + lock (_mutex) { - lock (_mutex) - { - if (_tcs.Task.IsCompleted) - _tcs = TaskCompletionSourceExtensions.CreateAsyncTaskSource(); - } + if (_tcs.Task.IsCompleted) + _tcs = TaskCompletionSourceExtensions.CreateAsyncTaskSource(); } + } - // ReSharper disable UnusedMember.Local - [DebuggerNonUserCode] - private sealed class DebugView - { - private readonly AsyncManualResetEvent _mre; + // ReSharper disable UnusedMember.Local + [DebuggerNonUserCode] + private sealed class DebugView + { + private readonly AsyncManualResetEvent _mre; - public DebugView(AsyncManualResetEvent mre) - { - _mre = mre; - } + public DebugView(AsyncManualResetEvent mre) + { + _mre = mre; + } - public int Id { get { return _mre.Id; } } + public int Id { get { return _mre.Id; } } - public bool IsSet { get { return _mre.GetStateForDebugger; } } + public bool IsSet { get { return _mre.GetStateForDebugger; } } - public Task CurrentTask { get { return _mre._tcs.Task; } } - } - // ReSharper restore UnusedMember.Local + public Task CurrentTask { get { return _mre._tcs.Task; } } } + // ReSharper restore UnusedMember.Local } diff --git a/src/Foundatio/Nito.AsyncEx.Coordination/AsyncReaderWriterLock.cs b/src/Foundatio/Nito.AsyncEx.Coordination/AsyncReaderWriterLock.cs index f66a7efe3..dc6b2b082 100644 --- a/src/Foundatio/Nito.AsyncEx.Coordination/AsyncReaderWriterLock.cs +++ b/src/Foundatio/Nito.AsyncEx.Coordination/AsyncReaderWriterLock.cs @@ -6,342 +6,341 @@ // Original idea from Stephen Toub: http://blogs.msdn.com/b/pfxteam/archive/2012/02/12/building-async-coordination-primitives-part-7-asyncreaderwriterlock.aspx -namespace Foundatio.AsyncEx +namespace Foundatio.AsyncEx; + +/// +/// A reader/writer lock that is compatible with async. Note that this lock is not recursive! +/// +[DebuggerDisplay("Id = {Id}, State = {GetStateForDebugger}, ReaderCount = {GetReaderCountForDebugger}")] +[DebuggerTypeProxy(typeof(DebugView))] +public sealed class AsyncReaderWriterLock { /// - /// A reader/writer lock that is compatible with async. Note that this lock is not recursive! + /// The queue of TCSs that other tasks are awaiting to acquire the lock as writers. /// - [DebuggerDisplay("Id = {Id}, State = {GetStateForDebugger}, ReaderCount = {GetReaderCountForDebugger}")] - [DebuggerTypeProxy(typeof(DebugView))] - public sealed class AsyncReaderWriterLock - { - /// - /// The queue of TCSs that other tasks are awaiting to acquire the lock as writers. - /// - private readonly IAsyncWaitQueue _writerQueue; + private readonly IAsyncWaitQueue _writerQueue; - /// - /// The queue of TCSs that other tasks are awaiting to acquire the lock as readers. - /// - private readonly IAsyncWaitQueue _readerQueue; + /// + /// The queue of TCSs that other tasks are awaiting to acquire the lock as readers. + /// + private readonly IAsyncWaitQueue _readerQueue; - /// - /// The object used for mutual exclusion. - /// - private readonly object _mutex; + /// + /// The object used for mutual exclusion. + /// + private readonly object _mutex; - /// - /// The semi-unique identifier for this instance. This is 0 if the id has not yet been created. - /// - private int _id; + /// + /// The semi-unique identifier for this instance. This is 0 if the id has not yet been created. + /// + private int _id; - /// - /// Number of reader locks held; -1 if a writer lock is held; 0 if no locks are held. - /// - private int _locksHeld; + /// + /// Number of reader locks held; -1 if a writer lock is held; 0 if no locks are held. + /// + private int _locksHeld; - [DebuggerNonUserCode] - internal State GetStateForDebugger + [DebuggerNonUserCode] + internal State GetStateForDebugger + { + get { - get - { - if (_locksHeld == 0) - return State.Unlocked; - if (_locksHeld == -1) - return State.WriteLocked; - return State.ReadLocked; - } + if (_locksHeld == 0) + return State.Unlocked; + if (_locksHeld == -1) + return State.WriteLocked; + return State.ReadLocked; } + } - internal enum State - { - Unlocked, - ReadLocked, - WriteLocked, - } + internal enum State + { + Unlocked, + ReadLocked, + WriteLocked, + } - [DebuggerNonUserCode] - internal int GetReaderCountForDebugger { get { return (_locksHeld > 0 ? _locksHeld : 0); } } + [DebuggerNonUserCode] + internal int GetReaderCountForDebugger { get { return (_locksHeld > 0 ? _locksHeld : 0); } } - /// - /// Creates a new async-compatible reader/writer lock. - /// - /// The wait queue used to manage waiters for writer locks. This may be null to use a default (FIFO) queue. - /// The wait queue used to manage waiters for reader locks. This may be null to use a default (FIFO) queue. - internal AsyncReaderWriterLock(IAsyncWaitQueue writerQueue, IAsyncWaitQueue readerQueue) - { - _writerQueue = writerQueue ?? new DefaultAsyncWaitQueue(); - _readerQueue = readerQueue ?? new DefaultAsyncWaitQueue(); - _mutex = new object(); - } + /// + /// Creates a new async-compatible reader/writer lock. + /// + /// The wait queue used to manage waiters for writer locks. This may be null to use a default (FIFO) queue. + /// The wait queue used to manage waiters for reader locks. This may be null to use a default (FIFO) queue. + internal AsyncReaderWriterLock(IAsyncWaitQueue writerQueue, IAsyncWaitQueue readerQueue) + { + _writerQueue = writerQueue ?? new DefaultAsyncWaitQueue(); + _readerQueue = readerQueue ?? new DefaultAsyncWaitQueue(); + _mutex = new object(); + } - /// - /// Creates a new async-compatible reader/writer lock. - /// - public AsyncReaderWriterLock() - : this(null, null) - { - } + /// + /// Creates a new async-compatible reader/writer lock. + /// + public AsyncReaderWriterLock() + : this(null, null) + { + } - /// - /// Gets a semi-unique identifier for this asynchronous lock. - /// - public int Id - { - get { return IdManager.GetId(ref _id); } - } + /// + /// Gets a semi-unique identifier for this asynchronous lock. + /// + public int Id + { + get { return IdManager.GetId(ref _id); } + } - /// - /// Applies a continuation to the task that will call if the task is canceled. This method may not be called while holding the sync lock. - /// - /// The task to observe for cancellation. - private void ReleaseWaitersWhenCanceled(Task task) + /// + /// Applies a continuation to the task that will call if the task is canceled. This method may not be called while holding the sync lock. + /// + /// The task to observe for cancellation. + private void ReleaseWaitersWhenCanceled(Task task) + { + task.ContinueWith(t => { - task.ContinueWith(t => - { - lock (_mutex) { ReleaseWaiters(); } - }, CancellationToken.None, TaskContinuationOptions.OnlyOnCanceled | TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); - } + lock (_mutex) { ReleaseWaiters(); } + }, CancellationToken.None, TaskContinuationOptions.OnlyOnCanceled | TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); + } - /// - /// Asynchronously acquires the lock as a reader. Returns a disposable that releases the lock when disposed. - /// - /// The cancellation token used to cancel the lock. If this is already set, then this method will attempt to take the lock immediately (succeeding if the lock is currently available). - /// A disposable that releases the lock when disposed. - private Task RequestReaderLockAsync(CancellationToken cancellationToken) + /// + /// Asynchronously acquires the lock as a reader. Returns a disposable that releases the lock when disposed. + /// + /// The cancellation token used to cancel the lock. If this is already set, then this method will attempt to take the lock immediately (succeeding if the lock is currently available). + /// A disposable that releases the lock when disposed. + private Task RequestReaderLockAsync(CancellationToken cancellationToken) + { + lock (_mutex) { - lock (_mutex) + // If the lock is available or in read mode and there are no waiting writers, upgradeable readers, or upgrading readers, take it immediately. + if (_locksHeld >= 0 && _writerQueue.IsEmpty) + { + ++_locksHeld; + return Task.FromResult(new ReaderKey(this)); + } + else { - // If the lock is available or in read mode and there are no waiting writers, upgradeable readers, or upgrading readers, take it immediately. - if (_locksHeld >= 0 && _writerQueue.IsEmpty) - { - ++_locksHeld; - return Task.FromResult(new ReaderKey(this)); - } - else - { - // Wait for the lock to become available or cancellation. - return _readerQueue.Enqueue(_mutex, cancellationToken); - } + // Wait for the lock to become available or cancellation. + return _readerQueue.Enqueue(_mutex, cancellationToken); } } + } - /// - /// Asynchronously acquires the lock as a reader. Returns a disposable that releases the lock when disposed. - /// - /// The cancellation token used to cancel the lock. If this is already set, then this method will attempt to take the lock immediately (succeeding if the lock is currently available). - /// A disposable that releases the lock when disposed. - public AwaitableDisposable ReaderLockAsync(CancellationToken cancellationToken) - { - return new AwaitableDisposable(RequestReaderLockAsync(cancellationToken)); - } + /// + /// Asynchronously acquires the lock as a reader. Returns a disposable that releases the lock when disposed. + /// + /// The cancellation token used to cancel the lock. If this is already set, then this method will attempt to take the lock immediately (succeeding if the lock is currently available). + /// A disposable that releases the lock when disposed. + public AwaitableDisposable ReaderLockAsync(CancellationToken cancellationToken) + { + return new AwaitableDisposable(RequestReaderLockAsync(cancellationToken)); + } - /// - /// Asynchronously acquires the lock as a reader. Returns a disposable that releases the lock when disposed. - /// - /// A disposable that releases the lock when disposed. - public AwaitableDisposable ReaderLockAsync() - { - return ReaderLockAsync(CancellationToken.None); - } + /// + /// Asynchronously acquires the lock as a reader. Returns a disposable that releases the lock when disposed. + /// + /// A disposable that releases the lock when disposed. + public AwaitableDisposable ReaderLockAsync() + { + return ReaderLockAsync(CancellationToken.None); + } - /// - /// Synchronously acquires the lock as a reader. Returns a disposable that releases the lock when disposed. This method may block the calling thread. - /// - /// The cancellation token used to cancel the lock. If this is already set, then this method will attempt to take the lock immediately (succeeding if the lock is currently available). - /// A disposable that releases the lock when disposed. - public IDisposable ReaderLock(CancellationToken cancellationToken) - { - return RequestReaderLockAsync(cancellationToken).WaitAndUnwrapException(); - } + /// + /// Synchronously acquires the lock as a reader. Returns a disposable that releases the lock when disposed. This method may block the calling thread. + /// + /// The cancellation token used to cancel the lock. If this is already set, then this method will attempt to take the lock immediately (succeeding if the lock is currently available). + /// A disposable that releases the lock when disposed. + public IDisposable ReaderLock(CancellationToken cancellationToken) + { + return RequestReaderLockAsync(cancellationToken).WaitAndUnwrapException(); + } - /// - /// Synchronously acquires the lock as a reader. Returns a disposable that releases the lock when disposed. This method may block the calling thread. - /// - /// A disposable that releases the lock when disposed. - public IDisposable ReaderLock() - { - return ReaderLock(CancellationToken.None); - } + /// + /// Synchronously acquires the lock as a reader. Returns a disposable that releases the lock when disposed. This method may block the calling thread. + /// + /// A disposable that releases the lock when disposed. + public IDisposable ReaderLock() + { + return ReaderLock(CancellationToken.None); + } - /// - /// Asynchronously acquires the lock as a writer. Returns a disposable that releases the lock when disposed. - /// - /// The cancellation token used to cancel the lock. If this is already set, then this method will attempt to take the lock immediately (succeeding if the lock is currently available). - /// A disposable that releases the lock when disposed. - private Task RequestWriterLockAsync(CancellationToken cancellationToken) + /// + /// Asynchronously acquires the lock as a writer. Returns a disposable that releases the lock when disposed. + /// + /// The cancellation token used to cancel the lock. If this is already set, then this method will attempt to take the lock immediately (succeeding if the lock is currently available). + /// A disposable that releases the lock when disposed. + private Task RequestWriterLockAsync(CancellationToken cancellationToken) + { + Task ret; + lock (_mutex) { - Task ret; - lock (_mutex) + // If the lock is available, take it immediately. + if (_locksHeld == 0) { - // If the lock is available, take it immediately. - if (_locksHeld == 0) - { - _locksHeld = -1; - ret = Task.FromResult(new WriterKey(this)); - } - else - { - // Wait for the lock to become available or cancellation. - ret = _writerQueue.Enqueue(_mutex, cancellationToken); - } + _locksHeld = -1; + ret = Task.FromResult(new WriterKey(this)); + } + else + { + // Wait for the lock to become available or cancellation. + ret = _writerQueue.Enqueue(_mutex, cancellationToken); } - - ReleaseWaitersWhenCanceled(ret); - return ret; } - /// - /// Asynchronously acquires the lock as a writer. Returns a disposable that releases the lock when disposed. - /// - /// The cancellation token used to cancel the lock. If this is already set, then this method will attempt to take the lock immediately (succeeding if the lock is currently available). - /// A disposable that releases the lock when disposed. - public AwaitableDisposable WriterLockAsync(CancellationToken cancellationToken) - { - return new AwaitableDisposable(RequestWriterLockAsync(cancellationToken)); - } + ReleaseWaitersWhenCanceled(ret); + return ret; + } - /// - /// Asynchronously acquires the lock as a writer. Returns a disposable that releases the lock when disposed. - /// - /// A disposable that releases the lock when disposed. - public AwaitableDisposable WriterLockAsync() - { - return WriterLockAsync(CancellationToken.None); - } + /// + /// Asynchronously acquires the lock as a writer. Returns a disposable that releases the lock when disposed. + /// + /// The cancellation token used to cancel the lock. If this is already set, then this method will attempt to take the lock immediately (succeeding if the lock is currently available). + /// A disposable that releases the lock when disposed. + public AwaitableDisposable WriterLockAsync(CancellationToken cancellationToken) + { + return new AwaitableDisposable(RequestWriterLockAsync(cancellationToken)); + } - /// - /// Synchronously acquires the lock as a writer. Returns a disposable that releases the lock when disposed. This method may block the calling thread. - /// - /// The cancellation token used to cancel the lock. If this is already set, then this method will attempt to take the lock immediately (succeeding if the lock is currently available). - /// A disposable that releases the lock when disposed. - public IDisposable WriterLock(CancellationToken cancellationToken) - { - return RequestWriterLockAsync(cancellationToken).WaitAndUnwrapException(); - } + /// + /// Asynchronously acquires the lock as a writer. Returns a disposable that releases the lock when disposed. + /// + /// A disposable that releases the lock when disposed. + public AwaitableDisposable WriterLockAsync() + { + return WriterLockAsync(CancellationToken.None); + } - /// - /// Asynchronously acquires the lock as a writer. Returns a disposable that releases the lock when disposed. This method may block the calling thread. - /// - /// A disposable that releases the lock when disposed. - public IDisposable WriterLock() - { - return WriterLock(CancellationToken.None); - } + /// + /// Synchronously acquires the lock as a writer. Returns a disposable that releases the lock when disposed. This method may block the calling thread. + /// + /// The cancellation token used to cancel the lock. If this is already set, then this method will attempt to take the lock immediately (succeeding if the lock is currently available). + /// A disposable that releases the lock when disposed. + public IDisposable WriterLock(CancellationToken cancellationToken) + { + return RequestWriterLockAsync(cancellationToken).WaitAndUnwrapException(); + } - /// - /// Grants lock(s) to waiting tasks. This method assumes the sync lock is already held. - /// - private void ReleaseWaiters() - { - if (_locksHeld == -1) - return; + /// + /// Asynchronously acquires the lock as a writer. Returns a disposable that releases the lock when disposed. This method may block the calling thread. + /// + /// A disposable that releases the lock when disposed. + public IDisposable WriterLock() + { + return WriterLock(CancellationToken.None); + } + + /// + /// Grants lock(s) to waiting tasks. This method assumes the sync lock is already held. + /// + private void ReleaseWaiters() + { + if (_locksHeld == -1) + return; - // Give priority to writers, then readers. - if (!_writerQueue.IsEmpty) + // Give priority to writers, then readers. + if (!_writerQueue.IsEmpty) + { + if (_locksHeld == 0) { - if (_locksHeld == 0) - { - _locksHeld = -1; - _writerQueue.Dequeue(new WriterKey(this)); - return; - } + _locksHeld = -1; + _writerQueue.Dequeue(new WriterKey(this)); + return; } - else + } + else + { + while (!_readerQueue.IsEmpty) { - while (!_readerQueue.IsEmpty) - { - _readerQueue.Dequeue(new ReaderKey(this)); - ++_locksHeld; - } + _readerQueue.Dequeue(new ReaderKey(this)); + ++_locksHeld; } } + } - /// - /// Releases the lock as a reader. - /// - internal void ReleaseReaderLock() + /// + /// Releases the lock as a reader. + /// + internal void ReleaseReaderLock() + { + lock (_mutex) { - lock (_mutex) - { - --_locksHeld; - ReleaseWaiters(); - } + --_locksHeld; + ReleaseWaiters(); } + } - /// - /// Releases the lock as a writer. - /// - internal void ReleaseWriterLock() + /// + /// Releases the lock as a writer. + /// + internal void ReleaseWriterLock() + { + lock (_mutex) { - lock (_mutex) - { - _locksHeld = 0; - ReleaseWaiters(); - } + _locksHeld = 0; + ReleaseWaiters(); } + } + /// + /// The disposable which releases the reader lock. + /// + private sealed class ReaderKey : Disposables.SingleDisposable + { /// - /// The disposable which releases the reader lock. + /// Creates the key for a lock. /// - private sealed class ReaderKey : Disposables.SingleDisposable + /// The lock to release. May not be null. + public ReaderKey(AsyncReaderWriterLock asyncReaderWriterLock) + : base(asyncReaderWriterLock) { - /// - /// Creates the key for a lock. - /// - /// The lock to release. May not be null. - public ReaderKey(AsyncReaderWriterLock asyncReaderWriterLock) - : base(asyncReaderWriterLock) - { - } + } - protected override void Dispose(AsyncReaderWriterLock context) - { - context.ReleaseReaderLock(); - } + protected override void Dispose(AsyncReaderWriterLock context) + { + context.ReleaseReaderLock(); } + } + /// + /// The disposable which releases the writer lock. + /// + private sealed class WriterKey : Disposables.SingleDisposable + { /// - /// The disposable which releases the writer lock. + /// Creates the key for a lock. /// - private sealed class WriterKey : Disposables.SingleDisposable + /// The lock to release. May not be null. + public WriterKey(AsyncReaderWriterLock asyncReaderWriterLock) + : base(asyncReaderWriterLock) { - /// - /// Creates the key for a lock. - /// - /// The lock to release. May not be null. - public WriterKey(AsyncReaderWriterLock asyncReaderWriterLock) - : base(asyncReaderWriterLock) - { - } - - protected override void Dispose(AsyncReaderWriterLock context) - { - context.ReleaseWriterLock(); - } } - // ReSharper disable UnusedMember.Local - [DebuggerNonUserCode] - private sealed class DebugView + protected override void Dispose(AsyncReaderWriterLock context) { - private readonly AsyncReaderWriterLock _rwl; + context.ReleaseWriterLock(); + } + } - public DebugView(AsyncReaderWriterLock rwl) - { - _rwl = rwl; - } + // ReSharper disable UnusedMember.Local + [DebuggerNonUserCode] + private sealed class DebugView + { + private readonly AsyncReaderWriterLock _rwl; + + public DebugView(AsyncReaderWriterLock rwl) + { + _rwl = rwl; + } - public int Id { get { return _rwl.Id; } } + public int Id { get { return _rwl.Id; } } - public State State { get { return _rwl.GetStateForDebugger; } } + public State State { get { return _rwl.GetStateForDebugger; } } - public int ReaderCount { get { return _rwl.GetReaderCountForDebugger; } } + public int ReaderCount { get { return _rwl.GetReaderCountForDebugger; } } - public IAsyncWaitQueue ReaderWaitQueue { get { return _rwl._readerQueue; } } + public IAsyncWaitQueue ReaderWaitQueue { get { return _rwl._readerQueue; } } - public IAsyncWaitQueue WriterWaitQueue { get { return _rwl._writerQueue; } } - } - // ReSharper restore UnusedMember.Local + public IAsyncWaitQueue WriterWaitQueue { get { return _rwl._writerQueue; } } } + // ReSharper restore UnusedMember.Local } diff --git a/src/Foundatio/Nito.AsyncEx.Coordination/AsyncSemaphore.cs b/src/Foundatio/Nito.AsyncEx.Coordination/AsyncSemaphore.cs index 919616043..66a169ee9 100644 --- a/src/Foundatio/Nito.AsyncEx.Coordination/AsyncSemaphore.cs +++ b/src/Foundatio/Nito.AsyncEx.Coordination/AsyncSemaphore.cs @@ -6,206 +6,205 @@ // Original idea from Stephen Toub: http://blogs.msdn.com/b/pfxteam/archive/2012/02/12/10266983.aspx -namespace Foundatio.AsyncEx +namespace Foundatio.AsyncEx; + +/// +/// An async-compatible semaphore. Alternatively, you could use SemaphoreSlim. +/// +[DebuggerDisplay("Id = {Id}, CurrentCount = {_count}")] +[DebuggerTypeProxy(typeof(DebugView))] +public sealed class AsyncSemaphore { /// - /// An async-compatible semaphore. Alternatively, you could use SemaphoreSlim. + /// The queue of TCSs that other tasks are awaiting to acquire the semaphore. /// - [DebuggerDisplay("Id = {Id}, CurrentCount = {_count}")] - [DebuggerTypeProxy(typeof(DebugView))] - public sealed class AsyncSemaphore + private readonly IAsyncWaitQueue _queue; + + /// + /// The number of waits that will be immediately granted. + /// + private long _count; + + /// + /// The semi-unique identifier for this instance. This is 0 if the id has not yet been created. + /// + private int _id; + + /// + /// The object used for mutual exclusion. + /// + private readonly object _mutex; + + /// + /// Creates a new async-compatible semaphore with the specified initial count. + /// + /// The initial count for this semaphore. This must be greater than or equal to zero. + /// The wait queue used to manage waiters. This may be null to use a default (FIFO) queue. + internal AsyncSemaphore(long initialCount, IAsyncWaitQueue queue) { - /// - /// The queue of TCSs that other tasks are awaiting to acquire the semaphore. - /// - private readonly IAsyncWaitQueue _queue; - - /// - /// The number of waits that will be immediately granted. - /// - private long _count; - - /// - /// The semi-unique identifier for this instance. This is 0 if the id has not yet been created. - /// - private int _id; - - /// - /// The object used for mutual exclusion. - /// - private readonly object _mutex; - - /// - /// Creates a new async-compatible semaphore with the specified initial count. - /// - /// The initial count for this semaphore. This must be greater than or equal to zero. - /// The wait queue used to manage waiters. This may be null to use a default (FIFO) queue. - internal AsyncSemaphore(long initialCount, IAsyncWaitQueue queue) - { - _queue = queue ?? new DefaultAsyncWaitQueue(); - _count = initialCount; - _mutex = new object(); - } + _queue = queue ?? new DefaultAsyncWaitQueue(); + _count = initialCount; + _mutex = new object(); + } - /// - /// Creates a new async-compatible semaphore with the specified initial count. - /// - /// The initial count for this semaphore. This must be greater than or equal to zero. - public AsyncSemaphore(long initialCount) - : this(initialCount, null) - { - } + /// + /// Creates a new async-compatible semaphore with the specified initial count. + /// + /// The initial count for this semaphore. This must be greater than or equal to zero. + public AsyncSemaphore(long initialCount) + : this(initialCount, null) + { + } - /// - /// Gets a semi-unique identifier for this asynchronous semaphore. - /// - public int Id - { - get { return IdManager.GetId(ref _id); } - } + /// + /// Gets a semi-unique identifier for this asynchronous semaphore. + /// + public int Id + { + get { return IdManager.GetId(ref _id); } + } - /// - /// Gets the number of slots currently available on this semaphore. This member is seldom used; code using this member has a high possibility of race conditions. - /// - public long CurrentCount - { - get { lock (_mutex) { return _count; } } - } + /// + /// Gets the number of slots currently available on this semaphore. This member is seldom used; code using this member has a high possibility of race conditions. + /// + public long CurrentCount + { + get { lock (_mutex) { return _count; } } + } - /// - /// Asynchronously waits for a slot in the semaphore to be available. - /// - /// The cancellation token used to cancel the wait. If this is already set, then this method will attempt to take the slot immediately (succeeding if a slot is currently available). - public Task WaitAsync(CancellationToken cancellationToken) + /// + /// Asynchronously waits for a slot in the semaphore to be available. + /// + /// The cancellation token used to cancel the wait. If this is already set, then this method will attempt to take the slot immediately (succeeding if a slot is currently available). + public Task WaitAsync(CancellationToken cancellationToken) + { + Task ret; + lock (_mutex) { - Task ret; - lock (_mutex) + // If the semaphore is available, take it immediately and return. + if (_count != 0) { - // If the semaphore is available, take it immediately and return. - if (_count != 0) - { - --_count; - ret = TaskConstants.Completed; - } - else - { - // Wait for the semaphore to become available or cancellation. - ret = _queue.Enqueue(_mutex, cancellationToken); - } + --_count; + ret = TaskConstants.Completed; + } + else + { + // Wait for the semaphore to become available or cancellation. + ret = _queue.Enqueue(_mutex, cancellationToken); } - - return ret; } - /// - /// Asynchronously waits for a slot in the semaphore to be available. - /// - public Task WaitAsync() - { - return WaitAsync(CancellationToken.None); - } + return ret; + } - /// - /// Synchronously waits for a slot in the semaphore to be available. This method may block the calling thread. - /// - /// The cancellation token used to cancel the wait. If this is already set, then this method will attempt to take the slot immediately (succeeding if a slot is currently available). - public void Wait(CancellationToken cancellationToken) - { - WaitAsync(cancellationToken).WaitAndUnwrapException(); - } + /// + /// Asynchronously waits for a slot in the semaphore to be available. + /// + public Task WaitAsync() + { + return WaitAsync(CancellationToken.None); + } - /// - /// Synchronously waits for a slot in the semaphore to be available. This method may block the calling thread. - /// - public void Wait() - { - Wait(CancellationToken.None); - } + /// + /// Synchronously waits for a slot in the semaphore to be available. This method may block the calling thread. + /// + /// The cancellation token used to cancel the wait. If this is already set, then this method will attempt to take the slot immediately (succeeding if a slot is currently available). + public void Wait(CancellationToken cancellationToken) + { + WaitAsync(cancellationToken).WaitAndUnwrapException(); + } + + /// + /// Synchronously waits for a slot in the semaphore to be available. This method may block the calling thread. + /// + public void Wait() + { + Wait(CancellationToken.None); + } + + /// + /// Releases the semaphore. + /// + public void Release(long releaseCount) + { + if (releaseCount == 0) + return; - /// - /// Releases the semaphore. - /// - public void Release(long releaseCount) + lock (_mutex) { - if (releaseCount == 0) - return; + checked + { + var test = _count + releaseCount; + } - lock (_mutex) + while (releaseCount != 0 && !_queue.IsEmpty) { - checked - { - var test = _count + releaseCount; - } - - while (releaseCount != 0 && !_queue.IsEmpty) - { - _queue.Dequeue(); - --releaseCount; - } - _count += releaseCount; + _queue.Dequeue(); + --releaseCount; } + _count += releaseCount; } + } - /// - /// Releases the semaphore. - /// - public void Release() - { - Release(1); - } + /// + /// Releases the semaphore. + /// + public void Release() + { + Release(1); + } - private async Task DoLockAsync(CancellationToken cancellationToken) - { - await WaitAsync(cancellationToken).ConfigureAwait(false); - return Disposables.AnonymousDisposable.Create(Release); - } + private async Task DoLockAsync(CancellationToken cancellationToken) + { + await WaitAsync(cancellationToken).ConfigureAwait(false); + return Disposables.AnonymousDisposable.Create(Release); + } - /// - /// Asynchronously waits on the semaphore, and returns a disposable that releases the semaphore when disposed, thus treating this semaphore as a "multi-lock". - /// - /// The cancellation token used to cancel the wait. If this is already set, then this method will attempt to take the slot immediately (succeeding if a slot is currently available). - public AwaitableDisposable LockAsync(CancellationToken cancellationToken) - { - return new AwaitableDisposable(DoLockAsync(cancellationToken)); - } + /// + /// Asynchronously waits on the semaphore, and returns a disposable that releases the semaphore when disposed, thus treating this semaphore as a "multi-lock". + /// + /// The cancellation token used to cancel the wait. If this is already set, then this method will attempt to take the slot immediately (succeeding if a slot is currently available). + public AwaitableDisposable LockAsync(CancellationToken cancellationToken) + { + return new AwaitableDisposable(DoLockAsync(cancellationToken)); + } - /// - /// Asynchronously waits on the semaphore, and returns a disposable that releases the semaphore when disposed, thus treating this semaphore as a "multi-lock". - /// - public AwaitableDisposable LockAsync() => LockAsync(CancellationToken.None); + /// + /// Asynchronously waits on the semaphore, and returns a disposable that releases the semaphore when disposed, thus treating this semaphore as a "multi-lock". + /// + public AwaitableDisposable LockAsync() => LockAsync(CancellationToken.None); - /// - /// Synchronously waits on the semaphore, and returns a disposable that releases the semaphore when disposed, thus treating this semaphore as a "multi-lock". - /// - /// The cancellation token used to cancel the wait. If this is already set, then this method will attempt to take the slot immediately (succeeding if a slot is currently available). - public IDisposable Lock(CancellationToken cancellationToken) - { - Wait(cancellationToken); - return Disposables.AnonymousDisposable.Create(Release); - } + /// + /// Synchronously waits on the semaphore, and returns a disposable that releases the semaphore when disposed, thus treating this semaphore as a "multi-lock". + /// + /// The cancellation token used to cancel the wait. If this is already set, then this method will attempt to take the slot immediately (succeeding if a slot is currently available). + public IDisposable Lock(CancellationToken cancellationToken) + { + Wait(cancellationToken); + return Disposables.AnonymousDisposable.Create(Release); + } - /// - /// Synchronously waits on the semaphore, and returns a disposable that releases the semaphore when disposed, thus treating this semaphore as a "multi-lock". - /// - public IDisposable Lock() => Lock(CancellationToken.None); + /// + /// Synchronously waits on the semaphore, and returns a disposable that releases the semaphore when disposed, thus treating this semaphore as a "multi-lock". + /// + public IDisposable Lock() => Lock(CancellationToken.None); - // ReSharper disable UnusedMember.Local - [DebuggerNonUserCode] - private sealed class DebugView - { - private readonly AsyncSemaphore _semaphore; + // ReSharper disable UnusedMember.Local + [DebuggerNonUserCode] + private sealed class DebugView + { + private readonly AsyncSemaphore _semaphore; - public DebugView(AsyncSemaphore semaphore) - { - _semaphore = semaphore; - } + public DebugView(AsyncSemaphore semaphore) + { + _semaphore = semaphore; + } - public int Id { get { return _semaphore.Id; } } + public int Id { get { return _semaphore.Id; } } - public long CurrentCount { get { return _semaphore._count; } } + public long CurrentCount { get { return _semaphore._count; } } - public IAsyncWaitQueue WaitQueue { get { return _semaphore._queue; } } - } - // ReSharper restore UnusedMember.Local + public IAsyncWaitQueue WaitQueue { get { return _semaphore._queue; } } } + // ReSharper restore UnusedMember.Local } diff --git a/src/Foundatio/Nito.AsyncEx.Coordination/AsyncWaitQueue.cs b/src/Foundatio/Nito.AsyncEx.Coordination/AsyncWaitQueue.cs index 9b913bb82..122f53dd9 100644 --- a/src/Foundatio/Nito.AsyncEx.Coordination/AsyncWaitQueue.cs +++ b/src/Foundatio/Nito.AsyncEx.Coordination/AsyncWaitQueue.cs @@ -5,162 +5,161 @@ using System.Threading.Tasks; using Foundatio.Collections; -namespace Foundatio.AsyncEx +namespace Foundatio.AsyncEx; + +/// +/// A collection of cancelable instances. Implementations must assume the caller is holding a lock. +/// +/// The type of the results. If this isn't needed, use . +internal interface IAsyncWaitQueue { /// - /// A collection of cancelable instances. Implementations must assume the caller is holding a lock. + /// Gets whether the queue is empty. /// - /// The type of the results. If this isn't needed, use . - internal interface IAsyncWaitQueue - { - /// - /// Gets whether the queue is empty. - /// - bool IsEmpty { get; } - - /// - /// Creates a new entry and queues it to this wait queue. The returned task must support both synchronous and asynchronous waits. - /// - /// The queued task. - Task Enqueue(); - - /// - /// Removes a single entry in the wait queue and completes it. This method may only be called if is false. The task continuations for the completed task must be executed asynchronously. - /// - /// The result used to complete the wait queue entry. If this isn't needed, use default(T). - void Dequeue(T result = default(T)); - - /// - /// Removes all entries in the wait queue and completes them. The task continuations for the completed tasks must be executed asynchronously. - /// - /// The result used to complete the wait queue entries. If this isn't needed, use default(T). - void DequeueAll(T result = default(T)); - - /// - /// Attempts to remove an entry from the wait queue and cancels it. The task continuations for the completed task must be executed asynchronously. - /// - /// The task to cancel. - /// The cancellation token to use to cancel the task. - bool TryCancel(Task task, CancellationToken cancellationToken); - - /// - /// Removes all entries from the wait queue and cancels them. The task continuations for the completed tasks must be executed asynchronously. - /// - /// The cancellation token to use to cancel the tasks. - void CancelAll(CancellationToken cancellationToken); - } + bool IsEmpty { get; } /// - /// Provides extension methods for wait queues. + /// Creates a new entry and queues it to this wait queue. The returned task must support both synchronous and asynchronous waits. /// - internal static class AsyncWaitQueueExtensions - { - /// - /// Creates a new entry and queues it to this wait queue. If the cancellation token is already canceled, this method immediately returns a canceled task without modifying the wait queue. - /// - /// The wait queue. - /// A synchronization object taken while cancelling the entry. - /// The token used to cancel the wait. - /// The queued task. - public static Task Enqueue(this IAsyncWaitQueue @this, object mutex, CancellationToken token) - { - if (token.IsCancellationRequested) - return Task.FromCanceled(token); + /// The queued task. + Task Enqueue(); - var ret = @this.Enqueue(); - if (!token.CanBeCanceled) - return ret; + /// + /// Removes a single entry in the wait queue and completes it. This method may only be called if is false. The task continuations for the completed task must be executed asynchronously. + /// + /// The result used to complete the wait queue entry. If this isn't needed, use default(T). + void Dequeue(T result = default(T)); - var registration = token.Register(() => - { - lock (mutex) - @this.TryCancel(ret, token); - }, useSynchronizationContext: false); - ret.ContinueWith(_ => registration.Dispose(), CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); - return ret; - } - } + /// + /// Removes all entries in the wait queue and completes them. The task continuations for the completed tasks must be executed asynchronously. + /// + /// The result used to complete the wait queue entries. If this isn't needed, use default(T). + void DequeueAll(T result = default(T)); /// - /// The default wait queue implementation, which uses a double-ended queue. + /// Attempts to remove an entry from the wait queue and cancels it. The task continuations for the completed task must be executed asynchronously. /// - /// The type of the results. If this isn't needed, use . - [DebuggerDisplay("Count = {Count}")] - [DebuggerTypeProxy(typeof(DefaultAsyncWaitQueue<>.DebugView))] - internal sealed class DefaultAsyncWaitQueue : IAsyncWaitQueue + /// The task to cancel. + /// The cancellation token to use to cancel the task. + bool TryCancel(Task task, CancellationToken cancellationToken); + + /// + /// Removes all entries from the wait queue and cancels them. The task continuations for the completed tasks must be executed asynchronously. + /// + /// The cancellation token to use to cancel the tasks. + void CancelAll(CancellationToken cancellationToken); +} + +/// +/// Provides extension methods for wait queues. +/// +internal static class AsyncWaitQueueExtensions +{ + /// + /// Creates a new entry and queues it to this wait queue. If the cancellation token is already canceled, this method immediately returns a canceled task without modifying the wait queue. + /// + /// The wait queue. + /// A synchronization object taken while cancelling the entry. + /// The token used to cancel the wait. + /// The queued task. + public static Task Enqueue(this IAsyncWaitQueue @this, object mutex, CancellationToken token) { - private readonly Deque> _queue = new(); + if (token.IsCancellationRequested) + return Task.FromCanceled(token); - private int Count - { - get { return _queue.Count; } - } + var ret = @this.Enqueue(); + if (!token.CanBeCanceled) + return ret; - bool IAsyncWaitQueue.IsEmpty + var registration = token.Register(() => { - get { return Count == 0; } - } + lock (mutex) + @this.TryCancel(ret, token); + }, useSynchronizationContext: false); + ret.ContinueWith(_ => registration.Dispose(), CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); + return ret; + } +} - Task IAsyncWaitQueue.Enqueue() - { - var tcs = TaskCompletionSourceExtensions.CreateAsyncTaskSource(); - _queue.AddToBack(tcs); - return tcs.Task; - } +/// +/// The default wait queue implementation, which uses a double-ended queue. +/// +/// The type of the results. If this isn't needed, use . +[DebuggerDisplay("Count = {Count}")] +[DebuggerTypeProxy(typeof(DefaultAsyncWaitQueue<>.DebugView))] +internal sealed class DefaultAsyncWaitQueue : IAsyncWaitQueue +{ + private readonly Deque> _queue = new(); - void IAsyncWaitQueue.Dequeue(T result) - { - _queue.RemoveFromFront().TrySetResult(result); - } + private int Count + { + get { return _queue.Count; } + } - void IAsyncWaitQueue.DequeueAll(T result) - { - foreach (var source in _queue) - source.TrySetResult(result); - _queue.Clear(); - } + bool IAsyncWaitQueue.IsEmpty + { + get { return Count == 0; } + } + + Task IAsyncWaitQueue.Enqueue() + { + var tcs = TaskCompletionSourceExtensions.CreateAsyncTaskSource(); + _queue.AddToBack(tcs); + return tcs.Task; + } + + void IAsyncWaitQueue.Dequeue(T result) + { + _queue.RemoveFromFront().TrySetResult(result); + } - bool IAsyncWaitQueue.TryCancel(Task task, CancellationToken cancellationToken) + void IAsyncWaitQueue.DequeueAll(T result) + { + foreach (var source in _queue) + source.TrySetResult(result); + _queue.Clear(); + } + + bool IAsyncWaitQueue.TryCancel(Task task, CancellationToken cancellationToken) + { + for (int i = 0; i != _queue.Count; ++i) { - for (int i = 0; i != _queue.Count; ++i) + if (_queue[i].Task == task) { - if (_queue[i].Task == task) - { - _queue[i].TrySetCanceled(cancellationToken); - _queue.RemoveAt(i); - return true; - } + _queue[i].TrySetCanceled(cancellationToken); + _queue.RemoveAt(i); + return true; } - return false; } + return false; + } + + void IAsyncWaitQueue.CancelAll(CancellationToken cancellationToken) + { + foreach (var source in _queue) + source.TrySetCanceled(cancellationToken); + _queue.Clear(); + } + + [DebuggerNonUserCode] + internal sealed class DebugView + { + private readonly DefaultAsyncWaitQueue _queue; - void IAsyncWaitQueue.CancelAll(CancellationToken cancellationToken) + public DebugView(DefaultAsyncWaitQueue queue) { - foreach (var source in _queue) - source.TrySetCanceled(cancellationToken); - _queue.Clear(); + _queue = queue; } - [DebuggerNonUserCode] - internal sealed class DebugView + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public Task[] Tasks { - private readonly DefaultAsyncWaitQueue _queue; - - public DebugView(DefaultAsyncWaitQueue queue) - { - _queue = queue; - } - - [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] - public Task[] Tasks + get { - get - { - var result = new List>(_queue._queue.Count); - foreach (var entry in _queue._queue) - result.Add(entry.Task); - return result.ToArray(); - } + var result = new List>(_queue._queue.Count); + foreach (var entry in _queue._queue) + result.Add(entry.Task); + return result.ToArray(); } } } diff --git a/src/Foundatio/Nito.AsyncEx.Coordination/IdManager.cs b/src/Foundatio/Nito.AsyncEx.Coordination/IdManager.cs index b39be89fa..1a58863ad 100644 --- a/src/Foundatio/Nito.AsyncEx.Coordination/IdManager.cs +++ b/src/Foundatio/Nito.AsyncEx.Coordination/IdManager.cs @@ -1,48 +1,47 @@ using System.Threading; -namespace Foundatio.AsyncEx +namespace Foundatio.AsyncEx; + +/// +/// Allocates Ids for instances on demand. 0 is an invalid/unassigned Id. Ids may be non-unique in very long-running systems. This is similar to the Id system used by and . +/// +/// The type for which ids are generated. +// ReSharper disable UnusedTypeParameter +internal static class IdManager +// ReSharper restore UnusedTypeParameter { /// - /// Allocates Ids for instances on demand. 0 is an invalid/unassigned Id. Ids may be non-unique in very long-running systems. This is similar to the Id system used by and . + /// The last id generated for this type. This is 0 if no ids have been generated. /// - /// The type for which ids are generated. -// ReSharper disable UnusedTypeParameter - internal static class IdManager - // ReSharper restore UnusedTypeParameter - { - /// - /// The last id generated for this type. This is 0 if no ids have been generated. - /// // ReSharper disable StaticFieldInGenericType - private static int _lastId; - // ReSharper restore StaticFieldInGenericType + private static int _lastId; + // ReSharper restore StaticFieldInGenericType - /// - /// Returns the id, allocating it if necessary. - /// - /// A reference to the field containing the id. - public static int GetId(ref int id) - { - // If the Id has already been assigned, just use it. - if (id != 0) - return id; + /// + /// Returns the id, allocating it if necessary. + /// + /// A reference to the field containing the id. + public static int GetId(ref int id) + { + // If the Id has already been assigned, just use it. + if (id != 0) + return id; - // Determine the new Id without modifying "id", since other threads may also be determining the new Id at the same time. - int newId; + // Determine the new Id without modifying "id", since other threads may also be determining the new Id at the same time. + int newId; - // The Increment is in a while loop to ensure we get a non-zero Id: - // If we are incrementing -1, then we want to skip over 0. - // If there are tons of Id allocations going on, we want to skip over 0 no matter how many times we get it. - do - { - newId = Interlocked.Increment(ref _lastId); - } while (newId == 0); + // The Increment is in a while loop to ensure we get a non-zero Id: + // If we are incrementing -1, then we want to skip over 0. + // If there are tons of Id allocations going on, we want to skip over 0 no matter how many times we get it. + do + { + newId = Interlocked.Increment(ref _lastId); + } while (newId == 0); - // Update the Id unless another thread already updated it. - Interlocked.CompareExchange(ref id, newId, 0); + // Update the Id unless another thread already updated it. + Interlocked.CompareExchange(ref id, newId, 0); - // Return the current Id, regardless of whether it's our new Id or a new Id from another thread. - return id; - } + // Return the current Id, regardless of whether it's our new Id or a new Id from another thread. + return id; } } diff --git a/src/Foundatio/Nito.AsyncEx.Tasks/AwaitableDisposable.cs b/src/Foundatio/Nito.AsyncEx.Tasks/AwaitableDisposable.cs index a8acc958a..b586eaf05 100644 --- a/src/Foundatio/Nito.AsyncEx.Tasks/AwaitableDisposable.cs +++ b/src/Foundatio/Nito.AsyncEx.Tasks/AwaitableDisposable.cs @@ -2,62 +2,61 @@ using System.Runtime.CompilerServices; using System.Threading.Tasks; -namespace Foundatio.AsyncEx +namespace Foundatio.AsyncEx; + +/// +/// An awaitable wrapper around a task whose result is disposable. The wrapper is not disposable, so this prevents usage errors like "using (MyAsync())" when the appropriate usage should be "using (await MyAsync())". +/// +/// The type of the result of the underlying task. +public struct AwaitableDisposable where T : IDisposable { /// - /// An awaitable wrapper around a task whose result is disposable. The wrapper is not disposable, so this prevents usage errors like "using (MyAsync())" when the appropriate usage should be "using (await MyAsync())". + /// The underlying task. /// - /// The type of the result of the underlying task. - public struct AwaitableDisposable where T : IDisposable - { - /// - /// The underlying task. - /// - private readonly Task _task; + private readonly Task _task; - /// - /// Initializes a new awaitable wrapper around the specified task. - /// - /// The underlying task to wrap. This may not be null. - public AwaitableDisposable(Task task) - { - if (task == null) - throw new ArgumentNullException(nameof(task)); - _task = task; - } + /// + /// Initializes a new awaitable wrapper around the specified task. + /// + /// The underlying task to wrap. This may not be null. + public AwaitableDisposable(Task task) + { + if (task == null) + throw new ArgumentNullException(nameof(task)); + _task = task; + } - /// - /// Returns the underlying task. - /// - public Task AsTask() - { - return _task; - } + /// + /// Returns the underlying task. + /// + public Task AsTask() + { + return _task; + } - /// - /// Implicit conversion to the underlying task. - /// - /// The awaitable wrapper. - public static implicit operator Task(AwaitableDisposable source) - { - return source.AsTask(); - } + /// + /// Implicit conversion to the underlying task. + /// + /// The awaitable wrapper. + public static implicit operator Task(AwaitableDisposable source) + { + return source.AsTask(); + } - /// - /// Infrastructure. Returns the task awaiter for the underlying task. - /// - public TaskAwaiter GetAwaiter() - { - return _task.GetAwaiter(); - } + /// + /// Infrastructure. Returns the task awaiter for the underlying task. + /// + public TaskAwaiter GetAwaiter() + { + return _task.GetAwaiter(); + } - /// - /// Infrastructure. Returns a configured task awaiter for the underlying task. - /// - /// Whether to attempt to marshal the continuation back to the captured context. - public ConfiguredTaskAwaitable ConfigureAwait(bool continueOnCapturedContext) - { - return _task.ConfigureAwait(continueOnCapturedContext); - } + /// + /// Infrastructure. Returns a configured task awaiter for the underlying task. + /// + /// Whether to attempt to marshal the continuation back to the captured context. + public ConfiguredTaskAwaitable ConfigureAwait(bool continueOnCapturedContext) + { + return _task.ConfigureAwait(continueOnCapturedContext); } } diff --git a/src/Foundatio/Nito.AsyncEx.Tasks/CancellationTokenTaskSource.cs b/src/Foundatio/Nito.AsyncEx.Tasks/CancellationTokenTaskSource.cs index 79695e260..b8038810b 100644 --- a/src/Foundatio/Nito.AsyncEx.Tasks/CancellationTokenTaskSource.cs +++ b/src/Foundatio/Nito.AsyncEx.Tasks/CancellationTokenTaskSource.cs @@ -2,45 +2,44 @@ using System.Threading; using System.Threading.Tasks; -namespace Foundatio.AsyncEx +namespace Foundatio.AsyncEx; + +/// +/// Holds the task for a cancellation token, as well as the token registration. The registration is disposed when this instance is disposed. +/// +public sealed class CancellationTokenTaskSource : IDisposable { /// - /// Holds the task for a cancellation token, as well as the token registration. The registration is disposed when this instance is disposed. + /// The cancellation token registration, if any. This is null if the registration was not necessary. /// - public sealed class CancellationTokenTaskSource : IDisposable - { - /// - /// The cancellation token registration, if any. This is null if the registration was not necessary. - /// - private readonly IDisposable _registration; + private readonly IDisposable _registration; - /// - /// Creates a task for the specified cancellation token, registering with the token if necessary. - /// - /// The cancellation token to observe. - public CancellationTokenTaskSource(CancellationToken cancellationToken) + /// + /// Creates a task for the specified cancellation token, registering with the token if necessary. + /// + /// The cancellation token to observe. + public CancellationTokenTaskSource(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) { - if (cancellationToken.IsCancellationRequested) - { - Task = System.Threading.Tasks.Task.FromCanceled(cancellationToken); - return; - } - var tcs = new TaskCompletionSource(); - _registration = cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken), useSynchronizationContext: false); - Task = tcs.Task; + Task = System.Threading.Tasks.Task.FromCanceled(cancellationToken); + return; } + var tcs = new TaskCompletionSource(); + _registration = cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken), useSynchronizationContext: false); + Task = tcs.Task; + } - /// - /// Gets the task for the source cancellation token. - /// - public Task Task { get; private set; } + /// + /// Gets the task for the source cancellation token. + /// + public Task Task { get; private set; } - /// - /// Disposes the cancellation token registration, if any. Note that this may cause to never complete. - /// - public void Dispose() - { - _registration?.Dispose(); - } + /// + /// Disposes the cancellation token registration, if any. Note that this may cause to never complete. + /// + public void Dispose() + { + _registration?.Dispose(); } } diff --git a/src/Foundatio/Nito.AsyncEx.Tasks/Synchronous/TaskExtensions.cs b/src/Foundatio/Nito.AsyncEx.Tasks/Synchronous/TaskExtensions.cs index c96d09fca..a6f5c7e1c 100644 --- a/src/Foundatio/Nito.AsyncEx.Tasks/Synchronous/TaskExtensions.cs +++ b/src/Foundatio/Nito.AsyncEx.Tasks/Synchronous/TaskExtensions.cs @@ -3,115 +3,114 @@ using System.Threading; using System.Threading.Tasks; -namespace Foundatio.AsyncEx.Synchronous +namespace Foundatio.AsyncEx.Synchronous; + +/// +/// Provides synchronous extension methods for tasks. +/// +public static class TaskExtensions { /// - /// Provides synchronous extension methods for tasks. + /// Waits for the task to complete, unwrapping any exceptions. + /// + /// The task. May not be null. + public static void WaitAndUnwrapException(this Task task) + { + if (task == null) + throw new ArgumentNullException(nameof(task)); + task.GetAwaiter().GetResult(); + } + + /// + /// Waits for the task to complete, unwrapping any exceptions. /// - public static class TaskExtensions + /// The task. May not be null. + /// A cancellation token to observe while waiting for the task to complete. + /// The was cancelled before the completed, or the raised an . + public static void WaitAndUnwrapException(this Task task, CancellationToken cancellationToken) { - /// - /// Waits for the task to complete, unwrapping any exceptions. - /// - /// The task. May not be null. - public static void WaitAndUnwrapException(this Task task) + if (task == null) + throw new ArgumentNullException(nameof(task)); + try { - if (task == null) - throw new ArgumentNullException(nameof(task)); - task.GetAwaiter().GetResult(); + task.Wait(cancellationToken); } - - /// - /// Waits for the task to complete, unwrapping any exceptions. - /// - /// The task. May not be null. - /// A cancellation token to observe while waiting for the task to complete. - /// The was cancelled before the completed, or the raised an . - public static void WaitAndUnwrapException(this Task task, CancellationToken cancellationToken) + catch (AggregateException ex) { - if (task == null) - throw new ArgumentNullException(nameof(task)); - try - { - task.Wait(cancellationToken); - } - catch (AggregateException ex) - { - throw ExceptionHelpers.PrepareForRethrow(ex.InnerException); - } + throw ExceptionHelpers.PrepareForRethrow(ex.InnerException); } + } - /// - /// Waits for the task to complete, unwrapping any exceptions. - /// - /// The type of the result of the task. - /// The task. May not be null. - /// The result of the task. - public static TResult WaitAndUnwrapException(this Task task) + /// + /// Waits for the task to complete, unwrapping any exceptions. + /// + /// The type of the result of the task. + /// The task. May not be null. + /// The result of the task. + public static TResult WaitAndUnwrapException(this Task task) + { + if (task == null) + throw new ArgumentNullException(nameof(task)); + return task.GetAwaiter().GetResult(); + } + + /// + /// Waits for the task to complete, unwrapping any exceptions. + /// + /// The type of the result of the task. + /// The task. May not be null. + /// A cancellation token to observe while waiting for the task to complete. + /// The result of the task. + /// The was cancelled before the completed, or the raised an . + public static TResult WaitAndUnwrapException(this Task task, CancellationToken cancellationToken) + { + if (task == null) + throw new ArgumentNullException(nameof(task)); + try { - if (task == null) - throw new ArgumentNullException(nameof(task)); - return task.GetAwaiter().GetResult(); + task.Wait(cancellationToken); + return task.Result; } - - /// - /// Waits for the task to complete, unwrapping any exceptions. - /// - /// The type of the result of the task. - /// The task. May not be null. - /// A cancellation token to observe while waiting for the task to complete. - /// The result of the task. - /// The was cancelled before the completed, or the raised an . - public static TResult WaitAndUnwrapException(this Task task, CancellationToken cancellationToken) + catch (AggregateException ex) { - if (task == null) - throw new ArgumentNullException(nameof(task)); - try - { - task.Wait(cancellationToken); - return task.Result; - } - catch (AggregateException ex) - { - throw ExceptionHelpers.PrepareForRethrow(ex.InnerException); - } + throw ExceptionHelpers.PrepareForRethrow(ex.InnerException); } + } - /// - /// Waits for the task to complete, but does not raise task exceptions. The task exception (if any) is unobserved. - /// - /// The task. May not be null. - public static void WaitWithoutException(this Task task) + /// + /// Waits for the task to complete, but does not raise task exceptions. The task exception (if any) is unobserved. + /// + /// The task. May not be null. + public static void WaitWithoutException(this Task task) + { + if (task == null) + throw new ArgumentNullException(nameof(task)); + try + { + task.Wait(); + } + catch (AggregateException) { - if (task == null) - throw new ArgumentNullException(nameof(task)); - try - { - task.Wait(); - } - catch (AggregateException) - { - } } + } - /// - /// Waits for the task to complete, but does not raise task exceptions. The task exception (if any) is unobserved. - /// - /// The task. May not be null. - /// A cancellation token to observe while waiting for the task to complete. - /// The was cancelled before the completed. - public static void WaitWithoutException(this Task task, CancellationToken cancellationToken) + /// + /// Waits for the task to complete, but does not raise task exceptions. The task exception (if any) is unobserved. + /// + /// The task. May not be null. + /// A cancellation token to observe while waiting for the task to complete. + /// The was cancelled before the completed. + public static void WaitWithoutException(this Task task, CancellationToken cancellationToken) + { + if (task == null) + throw new ArgumentNullException(nameof(task)); + try + { + task.Wait(cancellationToken); + } + catch (AggregateException) { - if (task == null) - throw new ArgumentNullException(nameof(task)); - try - { - task.Wait(cancellationToken); - } - catch (AggregateException) - { - cancellationToken.ThrowIfCancellationRequested(); - } + cancellationToken.ThrowIfCancellationRequested(); } } } diff --git a/src/Foundatio/Nito.AsyncEx.Tasks/TaskCompletionSourceExtensions.cs b/src/Foundatio/Nito.AsyncEx.Tasks/TaskCompletionSourceExtensions.cs index f3776660e..b68efa502 100644 --- a/src/Foundatio/Nito.AsyncEx.Tasks/TaskCompletionSourceExtensions.cs +++ b/src/Foundatio/Nito.AsyncEx.Tasks/TaskCompletionSourceExtensions.cs @@ -2,86 +2,85 @@ using System.Threading.Tasks; using Foundatio.AsyncEx.Synchronous; -namespace Foundatio.AsyncEx +namespace Foundatio.AsyncEx; + +/// +/// Provides extension methods for . +/// +public static class TaskCompletionSourceExtensions { /// - /// Provides extension methods for . + /// Attempts to complete a , propagating the completion of . /// - public static class TaskCompletionSourceExtensions + /// The type of the result of the target asynchronous operation. + /// The type of the result of the source asynchronous operation. + /// The task completion source. May not be null. + /// The task. May not be null. + /// true if this method completed the task completion source; false if it was already completed. + public static bool TryCompleteFromCompletedTask(this TaskCompletionSource @this, Task task) where TSourceResult : TResult { - /// - /// Attempts to complete a , propagating the completion of . - /// - /// The type of the result of the target asynchronous operation. - /// The type of the result of the source asynchronous operation. - /// The task completion source. May not be null. - /// The task. May not be null. - /// true if this method completed the task completion source; false if it was already completed. - public static bool TryCompleteFromCompletedTask(this TaskCompletionSource @this, Task task) where TSourceResult : TResult - { - if (@this == null) - throw new ArgumentNullException(nameof(@this)); - if (task == null) - throw new ArgumentNullException(nameof(task)); + if (@this == null) + throw new ArgumentNullException(nameof(@this)); + if (task == null) + throw new ArgumentNullException(nameof(task)); - if (task.IsFaulted) - return @this.TrySetException(task.Exception.InnerExceptions); - if (task.IsCanceled) + if (task.IsFaulted) + return @this.TrySetException(task.Exception.InnerExceptions); + if (task.IsCanceled) + { + try + { + task.WaitAndUnwrapException(); + } + catch (OperationCanceledException exception) { - try - { - task.WaitAndUnwrapException(); - } - catch (OperationCanceledException exception) - { - var token = exception.CancellationToken; - return token.IsCancellationRequested ? @this.TrySetCanceled(token) : @this.TrySetCanceled(); - } + var token = exception.CancellationToken; + return token.IsCancellationRequested ? @this.TrySetCanceled(token) : @this.TrySetCanceled(); } - return @this.TrySetResult(task.Result); } + return @this.TrySetResult(task.Result); + } - /// - /// Attempts to complete a , propagating the completion of but using the result value from if the task completed successfully. - /// - /// The type of the result of the target asynchronous operation. - /// The task completion source. May not be null. - /// The task. May not be null. - /// A delegate that returns the result with which to complete the task completion source, if the task completed successfully. May not be null. - /// true if this method completed the task completion source; false if it was already completed. - public static bool TryCompleteFromCompletedTask(this TaskCompletionSource @this, Task task, Func resultFunc) - { - if (@this == null) - throw new ArgumentNullException(nameof(@this)); - if (task == null) - throw new ArgumentNullException(nameof(task)); - if (resultFunc == null) - throw new ArgumentNullException(nameof(resultFunc)); + /// + /// Attempts to complete a , propagating the completion of but using the result value from if the task completed successfully. + /// + /// The type of the result of the target asynchronous operation. + /// The task completion source. May not be null. + /// The task. May not be null. + /// A delegate that returns the result with which to complete the task completion source, if the task completed successfully. May not be null. + /// true if this method completed the task completion source; false if it was already completed. + public static bool TryCompleteFromCompletedTask(this TaskCompletionSource @this, Task task, Func resultFunc) + { + if (@this == null) + throw new ArgumentNullException(nameof(@this)); + if (task == null) + throw new ArgumentNullException(nameof(task)); + if (resultFunc == null) + throw new ArgumentNullException(nameof(resultFunc)); - if (task.IsFaulted) - return @this.TrySetException(task.Exception.InnerExceptions); - if (task.IsCanceled) + if (task.IsFaulted) + return @this.TrySetException(task.Exception.InnerExceptions); + if (task.IsCanceled) + { + try + { + task.WaitAndUnwrapException(); + } + catch (OperationCanceledException exception) { - try - { - task.WaitAndUnwrapException(); - } - catch (OperationCanceledException exception) - { - var token = exception.CancellationToken; - return token.IsCancellationRequested ? @this.TrySetCanceled(token) : @this.TrySetCanceled(); - } + var token = exception.CancellationToken; + return token.IsCancellationRequested ? @this.TrySetCanceled(token) : @this.TrySetCanceled(); } - return @this.TrySetResult(resultFunc()); } + return @this.TrySetResult(resultFunc()); + } - /// - /// Creates a new TCS for use with async code, and which forces its continuations to execute asynchronously. - /// - /// The type of the result of the TCS. - public static TaskCompletionSource CreateAsyncTaskSource() - { - return new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - } + /// + /// Creates a new TCS for use with async code, and which forces its continuations to execute asynchronously. + /// + /// The type of the result of the TCS. + public static TaskCompletionSource CreateAsyncTaskSource() + { + return new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); } } diff --git a/src/Foundatio/Nito.AsyncEx.Tasks/TaskConstants.cs b/src/Foundatio/Nito.AsyncEx.Tasks/TaskConstants.cs index 7b8f200e6..1725e7811 100644 --- a/src/Foundatio/Nito.AsyncEx.Tasks/TaskConstants.cs +++ b/src/Foundatio/Nito.AsyncEx.Tasks/TaskConstants.cs @@ -1,112 +1,111 @@ using System.Threading; using System.Threading.Tasks; -namespace Foundatio.AsyncEx +namespace Foundatio.AsyncEx; + +/// +/// Provides completed task constants. +/// +public static class TaskConstants { + private static readonly Task booleanTrue = Task.FromResult(true); + private static readonly Task intNegativeOne = Task.FromResult(-1); + /// - /// Provides completed task constants. + /// A task that has been completed with the value true. /// - public static class TaskConstants + public static Task BooleanTrue { - private static readonly Task booleanTrue = Task.FromResult(true); - private static readonly Task intNegativeOne = Task.FromResult(-1); - - /// - /// A task that has been completed with the value true. - /// - public static Task BooleanTrue + get { - get - { - return booleanTrue; - } + return booleanTrue; } + } - /// - /// A task that has been completed with the value false. - /// - public static Task BooleanFalse + /// + /// A task that has been completed with the value false. + /// + public static Task BooleanFalse + { + get { - get - { - return TaskConstants.Default; - } + return TaskConstants.Default; } + } - /// - /// A task that has been completed with the value 0. - /// - public static Task Int32Zero + /// + /// A task that has been completed with the value 0. + /// + public static Task Int32Zero + { + get { - get - { - return TaskConstants.Default; - } + return TaskConstants.Default; } + } - /// - /// A task that has been completed with the value -1. - /// - public static Task Int32NegativeOne + /// + /// A task that has been completed with the value -1. + /// + public static Task Int32NegativeOne + { + get { - get - { - return intNegativeOne; - } + return intNegativeOne; } + } - /// - /// A that has been completed. - /// - public static Task Completed + /// + /// A that has been completed. + /// + public static Task Completed + { + get { - get - { - return Task.CompletedTask; - } + return Task.CompletedTask; } + } - /// - /// A task that has been canceled. - /// - public static Task Canceled + /// + /// A task that has been canceled. + /// + public static Task Canceled + { + get { - get - { - return TaskConstants.Canceled; - } + return TaskConstants.Canceled; } } +} + +/// +/// Provides completed task constants. +/// +/// The type of the task result. +public static class TaskConstants +{ + private static readonly Task defaultValue = Task.FromResult(default(T)); + private static readonly Task canceled = Task.FromCanceled(new CancellationToken(true)); /// - /// Provides completed task constants. + /// A task that has been completed with the default value of . /// - /// The type of the task result. - public static class TaskConstants + public static Task Default { - private static readonly Task defaultValue = Task.FromResult(default(T)); - private static readonly Task canceled = Task.FromCanceled(new CancellationToken(true)); - - /// - /// A task that has been completed with the default value of . - /// - public static Task Default + get { - get - { - return defaultValue; - } + return defaultValue; } + } - /// - /// A task that has been canceled. - /// - public static Task Canceled + /// + /// A task that has been canceled. + /// + public static Task Canceled + { + get { - get - { - return canceled; - } + return canceled; } } } diff --git a/src/Foundatio/Nito.AsyncEx.Tasks/TaskExtensions.cs b/src/Foundatio/Nito.AsyncEx.Tasks/TaskExtensions.cs index 09d3c79bb..63262aacf 100644 --- a/src/Foundatio/Nito.AsyncEx.Tasks/TaskExtensions.cs +++ b/src/Foundatio/Nito.AsyncEx.Tasks/TaskExtensions.cs @@ -5,34 +5,33 @@ using System.Threading; using System.Threading.Tasks; -namespace Foundatio.AsyncEx +namespace Foundatio.AsyncEx; + +/// +/// Provides extension methods for the and types. +/// +public static class TaskExtensions { /// - /// Provides extension methods for the and types. + /// Asynchronously waits for the task to complete, or for the cancellation token to be canceled. /// - public static class TaskExtensions + /// The task to wait for. May not be null. + /// The cancellation token that cancels the wait. + public static Task WaitAsync(this Task @this, CancellationToken cancellationToken) { - /// - /// Asynchronously waits for the task to complete, or for the cancellation token to be canceled. - /// - /// The task to wait for. May not be null. - /// The cancellation token that cancels the wait. - public static Task WaitAsync(this Task @this, CancellationToken cancellationToken) - { - if (@this == null) - throw new ArgumentNullException(nameof(@this)); + if (@this == null) + throw new ArgumentNullException(nameof(@this)); - if (!cancellationToken.CanBeCanceled) - return @this; - if (cancellationToken.IsCancellationRequested) - return Task.FromCanceled(cancellationToken); - return DoWaitAsync(@this, cancellationToken); - } + if (!cancellationToken.CanBeCanceled) + return @this; + if (cancellationToken.IsCancellationRequested) + return Task.FromCanceled(cancellationToken); + return DoWaitAsync(@this, cancellationToken); + } - private static async Task DoWaitAsync(Task task, CancellationToken cancellationToken) - { - using var cancelTaskSource = new CancellationTokenTaskSource(cancellationToken); - await await Task.WhenAny(task, cancelTaskSource.Task).ConfigureAwait(false); - } + private static async Task DoWaitAsync(Task task, CancellationToken cancellationToken) + { + using var cancelTaskSource = new CancellationTokenTaskSource(cancellationToken); + await await Task.WhenAny(task, cancelTaskSource.Task).ConfigureAwait(false); } } diff --git a/src/Foundatio/Nito.Collections.Deque/CollectionHelpers.cs b/src/Foundatio/Nito.Collections.Deque/CollectionHelpers.cs index 30208506f..ee40cfd6e 100644 --- a/src/Foundatio/Nito.Collections.Deque/CollectionHelpers.cs +++ b/src/Foundatio/Nito.Collections.Deque/CollectionHelpers.cs @@ -2,87 +2,86 @@ using System.Collections; using System.Collections.Generic; -namespace Foundatio.Collections +namespace Foundatio.Collections; + +internal static class CollectionHelpers { - internal static class CollectionHelpers + public static IReadOnlyCollection ReifyCollection(IEnumerable source) { - public static IReadOnlyCollection ReifyCollection(IEnumerable source) - { - if (source == null) - throw new ArgumentNullException(nameof(source)); + if (source == null) + throw new ArgumentNullException(nameof(source)); - var result = source as IReadOnlyCollection; - if (result != null) - return result; - var collection = source as ICollection; - if (collection != null) - return new CollectionWrapper(collection); - var nongenericCollection = source as ICollection; - if (nongenericCollection != null) - return new NongenericCollectionWrapper(nongenericCollection); + var result = source as IReadOnlyCollection; + if (result != null) + return result; + var collection = source as ICollection; + if (collection != null) + return new CollectionWrapper(collection); + var nongenericCollection = source as ICollection; + if (nongenericCollection != null) + return new NongenericCollectionWrapper(nongenericCollection); - return new List(source); - } - - private sealed class NongenericCollectionWrapper : IReadOnlyCollection - { - private readonly ICollection _collection; + return new List(source); + } - public NongenericCollectionWrapper(ICollection collection) - { - if (collection == null) - throw new ArgumentNullException(nameof(collection)); - _collection = collection; - } + private sealed class NongenericCollectionWrapper : IReadOnlyCollection + { + private readonly ICollection _collection; - public int Count - { - get - { - return _collection.Count; - } - } + public NongenericCollectionWrapper(ICollection collection) + { + if (collection == null) + throw new ArgumentNullException(nameof(collection)); + _collection = collection; + } - public IEnumerator GetEnumerator() + public int Count + { + get { - foreach (T item in _collection) - yield return item; + return _collection.Count; } + } - IEnumerator IEnumerable.GetEnumerator() - { - return _collection.GetEnumerator(); - } + public IEnumerator GetEnumerator() + { + foreach (T item in _collection) + yield return item; } - private sealed class CollectionWrapper : IReadOnlyCollection + IEnumerator IEnumerable.GetEnumerator() { - private readonly ICollection _collection; + return _collection.GetEnumerator(); + } + } - public CollectionWrapper(ICollection collection) - { - if (collection == null) - throw new ArgumentNullException(nameof(collection)); - _collection = collection; - } + private sealed class CollectionWrapper : IReadOnlyCollection + { + private readonly ICollection _collection; - public int Count - { - get - { - return _collection.Count; - } - } + public CollectionWrapper(ICollection collection) + { + if (collection == null) + throw new ArgumentNullException(nameof(collection)); + _collection = collection; + } - public IEnumerator GetEnumerator() + public int Count + { + get { - return _collection.GetEnumerator(); + return _collection.Count; } + } - IEnumerator IEnumerable.GetEnumerator() - { - return _collection.GetEnumerator(); - } + public IEnumerator GetEnumerator() + { + return _collection.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return _collection.GetEnumerator(); } } } diff --git a/src/Foundatio/Nito.Collections.Deque/Deque.cs b/src/Foundatio/Nito.Collections.Deque/Deque.cs index 76b86ab6d..9e4932a52 100644 --- a/src/Foundatio/Nito.Collections.Deque/Deque.cs +++ b/src/Foundatio/Nito.Collections.Deque/Deque.cs @@ -2,882 +2,881 @@ using System.Collections.Generic; using System.Diagnostics; -namespace Foundatio.Collections +namespace Foundatio.Collections; + +/// +/// A double-ended queue (deque), which provides O(1) indexed access, O(1) removals from the front and back, amortized O(1) insertions to the front and back, and O(N) insertions and removals anywhere else (with the operations getting slower as the index approaches the middle). +/// +/// The type of elements contained in the deque. +[DebuggerDisplay("Count = {Count}, Capacity = {Capacity}")] +[DebuggerTypeProxy(typeof(Deque<>.DebugView))] +public sealed class Deque : IList, IReadOnlyList, System.Collections.IList { /// - /// A double-ended queue (deque), which provides O(1) indexed access, O(1) removals from the front and back, amortized O(1) insertions to the front and back, and O(N) insertions and removals anywhere else (with the operations getting slower as the index approaches the middle). + /// The default capacity. /// - /// The type of elements contained in the deque. - [DebuggerDisplay("Count = {Count}, Capacity = {Capacity}")] - [DebuggerTypeProxy(typeof(Deque<>.DebugView))] - public sealed class Deque : IList, IReadOnlyList, System.Collections.IList - { - /// - /// The default capacity. - /// - private const int DefaultCapacity = 8; + private const int DefaultCapacity = 8; - /// - /// The circular _buffer that holds the view. - /// - private T[] _buffer; + /// + /// The circular _buffer that holds the view. + /// + private T[] _buffer; - /// - /// The offset into where the view begins. - /// - private int _offset; + /// + /// The offset into where the view begins. + /// + private int _offset; - /// - /// Initializes a new instance of the class with the specified capacity. - /// - /// The initial capacity. Must be greater than 0. - public Deque(int capacity) - { - if (capacity < 0) - throw new ArgumentOutOfRangeException(nameof(capacity), "Capacity may not be negative"); - _buffer = new T[capacity]; - } + /// + /// Initializes a new instance of the class with the specified capacity. + /// + /// The initial capacity. Must be greater than 0. + public Deque(int capacity) + { + if (capacity < 0) + throw new ArgumentOutOfRangeException(nameof(capacity), "Capacity may not be negative"); + _buffer = new T[capacity]; + } - /// - /// Initializes a new instance of the class with the elements from the specified collection. - /// - /// The collection. May not be null. - public Deque(IEnumerable collection) - { - if (collection == null) - throw new ArgumentNullException(nameof(collection)); + /// + /// Initializes a new instance of the class with the elements from the specified collection. + /// + /// The collection. May not be null. + public Deque(IEnumerable collection) + { + if (collection == null) + throw new ArgumentNullException(nameof(collection)); - var source = CollectionHelpers.ReifyCollection(collection); - var count = source.Count; - if (count > 0) - { - _buffer = new T[count]; - DoInsertRange(0, source); - } - else - { - _buffer = new T[DefaultCapacity]; - } + var source = CollectionHelpers.ReifyCollection(collection); + var count = source.Count; + if (count > 0) + { + _buffer = new T[count]; + DoInsertRange(0, source); } - - /// - /// Initializes a new instance of the class. - /// - public Deque() - : this(DefaultCapacity) + else { + _buffer = new T[DefaultCapacity]; } + } - #region GenericListImplementations + /// + /// Initializes a new instance of the class. + /// + public Deque() + : this(DefaultCapacity) + { + } - /// - /// Gets a value indicating whether this list is read-only. This implementation always returns false. - /// - /// true if this list is read-only; otherwise, false. - bool ICollection.IsReadOnly - { - get { return false; } - } + #region GenericListImplementations - /// - /// Gets or sets the item at the specified index. - /// - /// The index of the item to get or set. - /// is not a valid index in this list. - /// This property is set and the list is read-only. - public T this[int index] - { - get - { - CheckExistingIndexArgument(Count, index); - return DoGetItem(index); - } + /// + /// Gets a value indicating whether this list is read-only. This implementation always returns false. + /// + /// true if this list is read-only; otherwise, false. + bool ICollection.IsReadOnly + { + get { return false; } + } - set - { - CheckExistingIndexArgument(Count, index); - DoSetItem(index, value); - } + /// + /// Gets or sets the item at the specified index. + /// + /// The index of the item to get or set. + /// is not a valid index in this list. + /// This property is set and the list is read-only. + public T this[int index] + { + get + { + CheckExistingIndexArgument(Count, index); + return DoGetItem(index); } - /// - /// Inserts an item to this list at the specified index. - /// - /// The zero-based index at which should be inserted. - /// The object to insert into this list. - /// - /// is not a valid index in this list. - /// - /// - /// This list is read-only. - /// - public void Insert(int index, T item) - { - CheckNewIndexArgument(Count, index); - DoInsert(index, item); - } - - /// - /// Removes the item at the specified index. - /// - /// The zero-based index of the item to remove. - /// - /// is not a valid index in this list. - /// - /// - /// This list is read-only. - /// - public void RemoveAt(int index) + set { CheckExistingIndexArgument(Count, index); - DoRemoveAt(index); + DoSetItem(index, value); } + } - /// - /// Determines the index of a specific item in this list. - /// - /// The object to locate in this list. - /// The index of if found in this list; otherwise, -1. - public int IndexOf(T item) - { - var comparer = EqualityComparer.Default; - int ret = 0; - foreach (var sourceItem in this) - { - if (comparer.Equals(item, sourceItem)) - return ret; - ++ret; - } + /// + /// Inserts an item to this list at the specified index. + /// + /// The zero-based index at which should be inserted. + /// The object to insert into this list. + /// + /// is not a valid index in this list. + /// + /// + /// This list is read-only. + /// + public void Insert(int index, T item) + { + CheckNewIndexArgument(Count, index); + DoInsert(index, item); + } - return -1; - } + /// + /// Removes the item at the specified index. + /// + /// The zero-based index of the item to remove. + /// + /// is not a valid index in this list. + /// + /// + /// This list is read-only. + /// + public void RemoveAt(int index) + { + CheckExistingIndexArgument(Count, index); + DoRemoveAt(index); + } - /// - /// Adds an item to the end of this list. - /// - /// The object to add to this list. - /// - /// This list is read-only. - /// - void ICollection.Add(T item) + /// + /// Determines the index of a specific item in this list. + /// + /// The object to locate in this list. + /// The index of if found in this list; otherwise, -1. + public int IndexOf(T item) + { + var comparer = EqualityComparer.Default; + int ret = 0; + foreach (var sourceItem in this) { - DoInsert(Count, item); + if (comparer.Equals(item, sourceItem)) + return ret; + ++ret; } - /// - /// Determines whether this list contains a specific value. - /// - /// The object to locate in this list. - /// - /// true if is found in this list; otherwise, false. - /// - bool ICollection.Contains(T item) + return -1; + } + + /// + /// Adds an item to the end of this list. + /// + /// The object to add to this list. + /// + /// This list is read-only. + /// + void ICollection.Add(T item) + { + DoInsert(Count, item); + } + + /// + /// Determines whether this list contains a specific value. + /// + /// The object to locate in this list. + /// + /// true if is found in this list; otherwise, false. + /// + bool ICollection.Contains(T item) + { + var comparer = EqualityComparer.Default; + foreach (var entry in this) { - var comparer = EqualityComparer.Default; - foreach (var entry in this) - { - if (comparer.Equals(item, entry)) - return true; - } - return false; + if (comparer.Equals(item, entry)) + return true; } + return false; + } - /// - /// Copies the elements of this list to an , starting at a particular index. - /// - /// The one-dimensional that is the destination of the elements copied from this slice. The must have zero-based indexing. - /// The zero-based index in at which copying begins. - /// - /// is null. - /// - /// - /// is less than 0. - /// - /// - /// is equal to or greater than the length of . - /// -or- - /// The number of elements in the source is greater than the available space from to the end of the destination . - /// - void ICollection.CopyTo(T[] array, int arrayIndex) - { - if (array == null) - throw new ArgumentNullException(nameof(array)); - - int count = Count; - CheckRangeArguments(array.Length, arrayIndex, count); - CopyToArray(array, arrayIndex); - } - - /// - /// Copies the deque elemens into an array. The resulting array always has all the deque elements contiguously. - /// - /// The destination array. - /// The optional index in the destination array at which to begin writing. - private void CopyToArray(Array array, int arrayIndex = 0) - { - if (array == null) - throw new ArgumentNullException(nameof(array)); - - if (IsSplit) - { - // The existing buffer is split, so we have to copy it in parts - int length = Capacity - _offset; - Array.Copy(_buffer, _offset, array, arrayIndex, length); - Array.Copy(_buffer, 0, array, arrayIndex + length, Count - length); - } - else - { - // The existing buffer is whole - Array.Copy(_buffer, _offset, array, arrayIndex, Count); - } - } + /// + /// Copies the elements of this list to an , starting at a particular index. + /// + /// The one-dimensional that is the destination of the elements copied from this slice. The must have zero-based indexing. + /// The zero-based index in at which copying begins. + /// + /// is null. + /// + /// + /// is less than 0. + /// + /// + /// is equal to or greater than the length of . + /// -or- + /// The number of elements in the source is greater than the available space from to the end of the destination . + /// + void ICollection.CopyTo(T[] array, int arrayIndex) + { + if (array == null) + throw new ArgumentNullException(nameof(array)); - /// - /// Removes the first occurrence of a specific object from this list. - /// - /// The object to remove from this list. - /// - /// true if was successfully removed from this list; otherwise, false. This method also returns false if is not found in this list. - /// - /// - /// This list is read-only. - /// - public bool Remove(T item) - { - int index = IndexOf(item); - if (index == -1) - return false; + int count = Count; + CheckRangeArguments(array.Length, arrayIndex, count); + CopyToArray(array, arrayIndex); + } - DoRemoveAt(index); - return true; - } + /// + /// Copies the deque elemens into an array. The resulting array always has all the deque elements contiguously. + /// + /// The destination array. + /// The optional index in the destination array at which to begin writing. + private void CopyToArray(Array array, int arrayIndex = 0) + { + if (array == null) + throw new ArgumentNullException(nameof(array)); - /// - /// Returns an enumerator that iterates through the collection. - /// - /// - /// A that can be used to iterate through the collection. - /// - public IEnumerator GetEnumerator() + if (IsSplit) { - int count = Count; - for (int i = 0; i != count; ++i) - { - yield return DoGetItem(i); - } + // The existing buffer is split, so we have to copy it in parts + int length = Capacity - _offset; + Array.Copy(_buffer, _offset, array, arrayIndex, length); + Array.Copy(_buffer, 0, array, arrayIndex + length, Count - length); } - - /// - /// Returns an enumerator that iterates through a collection. - /// - /// - /// An object that can be used to iterate through the collection. - /// - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + else { - return GetEnumerator(); + // The existing buffer is whole + Array.Copy(_buffer, _offset, array, arrayIndex, Count); } + } - #endregion - #region ObjectListImplementations + /// + /// Removes the first occurrence of a specific object from this list. + /// + /// The object to remove from this list. + /// + /// true if was successfully removed from this list; otherwise, false. This method also returns false if is not found in this list. + /// + /// + /// This list is read-only. + /// + public bool Remove(T item) + { + int index = IndexOf(item); + if (index == -1) + return false; - private static bool IsT(object value) - { - if (value is T) - return true; - if (value != null) - return false; - return default(T) == null; - } + DoRemoveAt(index); + return true; + } - int System.Collections.IList.Add(object value) + /// + /// Returns an enumerator that iterates through the collection. + /// + /// + /// A that can be used to iterate through the collection. + /// + public IEnumerator GetEnumerator() + { + int count = Count; + for (int i = 0; i != count; ++i) { - if (value == null && default(T) != null) - throw new ArgumentNullException(nameof(value), "Value cannot be null"); - if (!IsT(value)) - throw new ArgumentException("Value is of incorrect type.", nameof(value)); - AddToBack((T)value); - return Count - 1; + yield return DoGetItem(i); } + } - bool System.Collections.IList.Contains(object value) - { - return IsT(value) ? ((ICollection)this).Contains((T)value) : false; - } + /// + /// Returns an enumerator that iterates through a collection. + /// + /// + /// An object that can be used to iterate through the collection. + /// + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + #endregion + #region ObjectListImplementations + + private static bool IsT(object value) + { + if (value is T) + return true; + if (value != null) + return false; + return default(T) == null; + } + + int System.Collections.IList.Add(object value) + { + if (value == null && default(T) != null) + throw new ArgumentNullException(nameof(value), "Value cannot be null"); + if (!IsT(value)) + throw new ArgumentException("Value is of incorrect type.", nameof(value)); + AddToBack((T)value); + return Count - 1; + } + + bool System.Collections.IList.Contains(object value) + { + return IsT(value) ? ((ICollection)this).Contains((T)value) : false; + } + + int System.Collections.IList.IndexOf(object value) + { + return IsT(value) ? IndexOf((T)value) : -1; + } + + void System.Collections.IList.Insert(int index, object value) + { + if (value == null && default(T) != null) + throw new ArgumentNullException("value", "Value cannot be null"); + if (!IsT(value)) + throw new ArgumentException("Value is of incorrect type.", "value"); + Insert(index, (T)value); + } + + bool System.Collections.IList.IsFixedSize + { + get { return false; } + } + + bool System.Collections.IList.IsReadOnly + { + get { return false; } + } - int System.Collections.IList.IndexOf(object value) + void System.Collections.IList.Remove(object value) + { + if (IsT(value)) + Remove((T)value); + } + + object System.Collections.IList.this[int index] + { + get { - return IsT(value) ? IndexOf((T)value) : -1; + return this[index]; } - void System.Collections.IList.Insert(int index, object value) + set { if (value == null && default(T) != null) - throw new ArgumentNullException("value", "Value cannot be null"); + throw new ArgumentNullException(nameof(value), "Value cannot be null"); if (!IsT(value)) - throw new ArgumentException("Value is of incorrect type.", "value"); - Insert(index, (T)value); + throw new ArgumentException("Value is of incorrect type.", nameof(value)); + this[index] = (T)value; } + } - bool System.Collections.IList.IsFixedSize - { - get { return false; } - } + void System.Collections.ICollection.CopyTo(Array array, int index) + { + if (array == null) + throw new ArgumentNullException(nameof(array), "Destination array cannot be null"); + CheckRangeArguments(array.Length, index, Count); - bool System.Collections.IList.IsReadOnly + try { - get { return false; } + CopyToArray(array, index); } - - void System.Collections.IList.Remove(object value) + catch (ArrayTypeMismatchException ex) { - if (IsT(value)) - Remove((T)value); + throw new ArgumentException("Destination array is of incorrect type.", nameof(array), ex); } - - object System.Collections.IList.this[int index] + catch (RankException ex) { - get - { - return this[index]; - } - - set - { - if (value == null && default(T) != null) - throw new ArgumentNullException(nameof(value), "Value cannot be null"); - if (!IsT(value)) - throw new ArgumentException("Value is of incorrect type.", nameof(value)); - this[index] = (T)value; - } + throw new ArgumentException("Destination array must be single dimensional.", nameof(array), ex); } + } - void System.Collections.ICollection.CopyTo(Array array, int index) - { - if (array == null) - throw new ArgumentNullException(nameof(array), "Destination array cannot be null"); - CheckRangeArguments(array.Length, index, Count); + bool System.Collections.ICollection.IsSynchronized + { + get { return false; } + } - try - { - CopyToArray(array, index); - } - catch (ArrayTypeMismatchException ex) - { - throw new ArgumentException("Destination array is of incorrect type.", nameof(array), ex); - } - catch (RankException ex) - { - throw new ArgumentException("Destination array must be single dimensional.", nameof(array), ex); - } - } + object System.Collections.ICollection.SyncRoot + { + get { return this; } + } - bool System.Collections.ICollection.IsSynchronized + #endregion + #region GenericListHelpers + + /// + /// Checks the argument to see if it refers to a valid insertion point in a source of a given length. + /// + /// The length of the source. This parameter is not checked for validity. + /// The index into the source. + /// is not a valid index to an insertion point for the source. + private static void CheckNewIndexArgument(int sourceLength, int index) + { + if (index < 0 || index > sourceLength) { - get { return false; } + throw new ArgumentOutOfRangeException(nameof(index), "Invalid new index " + index + " for source length " + sourceLength); } + } - object System.Collections.ICollection.SyncRoot + /// + /// Checks the argument to see if it refers to an existing element in a source of a given length. + /// + /// The length of the source. This parameter is not checked for validity. + /// The index into the source. + /// is not a valid index to an existing element for the source. + private static void CheckExistingIndexArgument(int sourceLength, int index) + { + if (index < 0 || index >= sourceLength) { - get { return this; } + throw new ArgumentOutOfRangeException(nameof(index), "Invalid existing index " + index + " for source length " + sourceLength); } + } - #endregion - #region GenericListHelpers - - /// - /// Checks the argument to see if it refers to a valid insertion point in a source of a given length. - /// - /// The length of the source. This parameter is not checked for validity. - /// The index into the source. - /// is not a valid index to an insertion point for the source. - private static void CheckNewIndexArgument(int sourceLength, int index) + /// + /// Checks the and arguments for validity when applied to a source of a given length. Allows 0-element ranges, including a 0-element range at the end of the source. + /// + /// The length of the source. This parameter is not checked for validity. + /// The index into source at which the range begins. + /// The number of elements in the range. + /// Either or is less than 0. + /// The range [offset, offset + count) is not within the range [0, sourceLength). + private static void CheckRangeArguments(int sourceLength, int offset, int count) + { + if (offset < 0) { - if (index < 0 || index > sourceLength) - { - throw new ArgumentOutOfRangeException(nameof(index), "Invalid new index " + index + " for source length " + sourceLength); - } + throw new ArgumentOutOfRangeException(nameof(offset), "Invalid offset " + offset); } - /// - /// Checks the argument to see if it refers to an existing element in a source of a given length. - /// - /// The length of the source. This parameter is not checked for validity. - /// The index into the source. - /// is not a valid index to an existing element for the source. - private static void CheckExistingIndexArgument(int sourceLength, int index) + if (count < 0) { - if (index < 0 || index >= sourceLength) - { - throw new ArgumentOutOfRangeException(nameof(index), "Invalid existing index " + index + " for source length " + sourceLength); - } + throw new ArgumentOutOfRangeException(nameof(count), "Invalid count " + count); } - /// - /// Checks the and arguments for validity when applied to a source of a given length. Allows 0-element ranges, including a 0-element range at the end of the source. - /// - /// The length of the source. This parameter is not checked for validity. - /// The index into source at which the range begins. - /// The number of elements in the range. - /// Either or is less than 0. - /// The range [offset, offset + count) is not within the range [0, sourceLength). - private static void CheckRangeArguments(int sourceLength, int offset, int count) + if (sourceLength - offset < count) { - if (offset < 0) - { - throw new ArgumentOutOfRangeException(nameof(offset), "Invalid offset " + offset); - } + throw new ArgumentException("Invalid offset (" + offset + ") or count + (" + count + ") for source length " + sourceLength); + } + } - if (count < 0) - { - throw new ArgumentOutOfRangeException(nameof(count), "Invalid count " + count); - } + #endregion - if (sourceLength - offset < count) - { - throw new ArgumentException("Invalid offset (" + offset + ") or count + (" + count + ") for source length " + sourceLength); - } - } + /// + /// Gets a value indicating whether this instance is empty. + /// + private bool IsEmpty + { + get { return Count == 0; } + } - #endregion + /// + /// Gets a value indicating whether this instance is at full capacity. + /// + private bool IsFull + { + get { return Count == Capacity; } + } - /// - /// Gets a value indicating whether this instance is empty. - /// - private bool IsEmpty + /// + /// Gets a value indicating whether the buffer is "split" (meaning the beginning of the view is at a later index in than the end). + /// + private bool IsSplit + { + get { - get { return Count == 0; } + // Overflow-safe version of "(offset + Count) > Capacity" + return _offset > (Capacity - Count); } + } - /// - /// Gets a value indicating whether this instance is at full capacity. - /// - private bool IsFull + /// + /// Gets or sets the capacity for this deque. This value must always be greater than zero, and this property cannot be set to a value less than . + /// + /// Capacity cannot be set to a value less than . + public int Capacity + { + get { - get { return Count == Capacity; } + return _buffer.Length; } - /// - /// Gets a value indicating whether the buffer is "split" (meaning the beginning of the view is at a later index in than the end). - /// - private bool IsSplit + set { - get - { - // Overflow-safe version of "(offset + Count) > Capacity" - return _offset > (Capacity - Count); - } - } + if (value < Count) + throw new ArgumentOutOfRangeException(nameof(value), "Capacity cannot be set to a value less than Count"); - /// - /// Gets or sets the capacity for this deque. This value must always be greater than zero, and this property cannot be set to a value less than . - /// - /// Capacity cannot be set to a value less than . - public int Capacity - { - get - { - return _buffer.Length; - } + if (value == _buffer.Length) + return; - set - { - if (value < Count) - throw new ArgumentOutOfRangeException(nameof(value), "Capacity cannot be set to a value less than Count"); + // Create the new _buffer and copy our existing range. + T[] newBuffer = new T[value]; + CopyToArray(newBuffer); - if (value == _buffer.Length) - return; + // Set up to use the new _buffer. + _buffer = newBuffer; + _offset = 0; + } + } - // Create the new _buffer and copy our existing range. - T[] newBuffer = new T[value]; - CopyToArray(newBuffer); + /// + /// Gets the number of elements contained in this deque. + /// + /// The number of elements contained in this deque. + public int Count { get; private set; } - // Set up to use the new _buffer. - _buffer = newBuffer; - _offset = 0; - } - } + /// + /// Applies the offset to , resulting in a buffer index. + /// + /// The deque index. + /// The buffer index. + private int DequeIndexToBufferIndex(int index) + { + return (index + _offset) % Capacity; + } - /// - /// Gets the number of elements contained in this deque. - /// - /// The number of elements contained in this deque. - public int Count { get; private set; } + /// + /// Gets an element at the specified view index. + /// + /// The zero-based view index of the element to get. This index is guaranteed to be valid. + /// The element at the specified index. + private T DoGetItem(int index) + { + return _buffer[DequeIndexToBufferIndex(index)]; + } - /// - /// Applies the offset to , resulting in a buffer index. - /// - /// The deque index. - /// The buffer index. - private int DequeIndexToBufferIndex(int index) + /// + /// Sets an element at the specified view index. + /// + /// The zero-based view index of the element to get. This index is guaranteed to be valid. + /// The element to store in the list. + private void DoSetItem(int index, T item) + { + _buffer[DequeIndexToBufferIndex(index)] = item; + } + + /// + /// Inserts an element at the specified view index. + /// + /// The zero-based view index at which the element should be inserted. This index is guaranteed to be valid. + /// The element to store in the list. + private void DoInsert(int index, T item) + { + EnsureCapacityForOneElement(); + + if (index == 0) { - return (index + _offset) % Capacity; + DoAddToFront(item); + return; } - - /// - /// Gets an element at the specified view index. - /// - /// The zero-based view index of the element to get. This index is guaranteed to be valid. - /// The element at the specified index. - private T DoGetItem(int index) + else if (index == Count) { - return _buffer[DequeIndexToBufferIndex(index)]; + DoAddToBack(item); + return; } - /// - /// Sets an element at the specified view index. - /// - /// The zero-based view index of the element to get. This index is guaranteed to be valid. - /// The element to store in the list. - private void DoSetItem(int index, T item) + DoInsertRange(index, new[] { item }); + } + + /// + /// Removes an element at the specified view index. + /// + /// The zero-based view index of the element to remove. This index is guaranteed to be valid. + private void DoRemoveAt(int index) + { + if (index == 0) { - _buffer[DequeIndexToBufferIndex(index)] = item; + DoRemoveFromFront(); + return; } - - /// - /// Inserts an element at the specified view index. - /// - /// The zero-based view index at which the element should be inserted. This index is guaranteed to be valid. - /// The element to store in the list. - private void DoInsert(int index, T item) + else if (index == Count - 1) { - EnsureCapacityForOneElement(); + DoRemoveFromBack(); + return; + } - if (index == 0) - { - DoAddToFront(item); - return; - } - else if (index == Count) - { - DoAddToBack(item); - return; - } + DoRemoveRange(index, 1); + } - DoInsertRange(index, new[] { item }); - } + /// + /// Increments by using modulo- arithmetic. + /// + /// The value by which to increase . May not be negative. + /// The value of after it was incremented. + private int PostIncrement(int value) + { + int ret = _offset; + _offset += value; + _offset %= Capacity; + return ret; + } - /// - /// Removes an element at the specified view index. - /// - /// The zero-based view index of the element to remove. This index is guaranteed to be valid. - private void DoRemoveAt(int index) - { - if (index == 0) - { - DoRemoveFromFront(); - return; - } - else if (index == Count - 1) - { - DoRemoveFromBack(); - return; - } + /// + /// Decrements by using modulo- arithmetic. + /// + /// The value by which to reduce . May not be negative or greater than . + /// The value of before it was decremented. + private int PreDecrement(int value) + { + _offset -= value; + if (_offset < 0) + _offset += Capacity; + return _offset; + } - DoRemoveRange(index, 1); - } + /// + /// Inserts a single element to the back of the view. must be false when this method is called. + /// + /// The element to insert. + private void DoAddToBack(T value) + { + _buffer[DequeIndexToBufferIndex(Count)] = value; + ++Count; + } - /// - /// Increments by using modulo- arithmetic. - /// - /// The value by which to increase . May not be negative. - /// The value of after it was incremented. - private int PostIncrement(int value) - { - int ret = _offset; - _offset += value; - _offset %= Capacity; - return ret; - } + /// + /// Inserts a single element to the front of the view. must be false when this method is called. + /// + /// The element to insert. + private void DoAddToFront(T value) + { + _buffer[PreDecrement(1)] = value; + ++Count; + } - /// - /// Decrements by using modulo- arithmetic. - /// - /// The value by which to reduce . May not be negative or greater than . - /// The value of before it was decremented. - private int PreDecrement(int value) + /// + /// Removes and returns the last element in the view. must be false when this method is called. + /// + /// The former last element. + private T DoRemoveFromBack() + { + T ret = _buffer[DequeIndexToBufferIndex(Count - 1)]; + --Count; + return ret; + } + + /// + /// Removes and returns the first element in the view. must be false when this method is called. + /// + /// The former first element. + private T DoRemoveFromFront() + { + --Count; + return _buffer[PostIncrement(1)]; + } + + /// + /// Inserts a range of elements into the view. + /// + /// The index into the view at which the elements are to be inserted. + /// The elements to insert. The sum of collection.Count and must be less than or equal to . + private void DoInsertRange(int index, IReadOnlyCollection collection) + { + var collectionCount = collection.Count; + // Make room in the existing list + if (index < Count / 2) { - _offset -= value; - if (_offset < 0) - _offset += Capacity; - return _offset; - } + // Inserting into the first half of the list + + // Move lower items down: [0, index) -> [Capacity - collectionCount, Capacity - collectionCount + index) + // This clears out the low "index" number of items, moving them "collectionCount" places down; + // after rotation, there will be a "collectionCount"-sized hole at "index". + int copyCount = index; + int writeIndex = Capacity - collectionCount; + for (int j = 0; j != copyCount; ++j) + _buffer[DequeIndexToBufferIndex(writeIndex + j)] = _buffer[DequeIndexToBufferIndex(j)]; - /// - /// Inserts a single element to the back of the view. must be false when this method is called. - /// - /// The element to insert. - private void DoAddToBack(T value) + // Rotate to the new view + PreDecrement(collectionCount); + } + else { - _buffer[DequeIndexToBufferIndex(Count)] = value; - ++Count; + // Inserting into the second half of the list + + // Move higher items up: [index, count) -> [index + collectionCount, collectionCount + count) + int copyCount = Count - index; + int writeIndex = index + collectionCount; + for (int j = copyCount - 1; j != -1; --j) + _buffer[DequeIndexToBufferIndex(writeIndex + j)] = _buffer[DequeIndexToBufferIndex(index + j)]; } - /// - /// Inserts a single element to the front of the view. must be false when this method is called. - /// - /// The element to insert. - private void DoAddToFront(T value) + // Copy new items into place + int i = index; + foreach (T item in collection) { - _buffer[PreDecrement(1)] = value; - ++Count; + _buffer[DequeIndexToBufferIndex(i)] = item; + ++i; } - /// - /// Removes and returns the last element in the view. must be false when this method is called. - /// - /// The former last element. - private T DoRemoveFromBack() + // Adjust valid count + Count += collectionCount; + } + + /// + /// Removes a range of elements from the view. + /// + /// The index into the view at which the range begins. + /// The number of elements in the range. This must be greater than 0 and less than or equal to . + private void DoRemoveRange(int index, int collectionCount) + { + if (index == 0) { - T ret = _buffer[DequeIndexToBufferIndex(Count - 1)]; - --Count; - return ret; + // Removing from the beginning: rotate to the new view + PostIncrement(collectionCount); + Count -= collectionCount; + return; } - - /// - /// Removes and returns the first element in the view. must be false when this method is called. - /// - /// The former first element. - private T DoRemoveFromFront() + else if (index == Count - collectionCount) { - --Count; - return _buffer[PostIncrement(1)]; + // Removing from the ending: trim the existing view + Count -= collectionCount; + return; } - /// - /// Inserts a range of elements into the view. - /// - /// The index into the view at which the elements are to be inserted. - /// The elements to insert. The sum of collection.Count and must be less than or equal to . - private void DoInsertRange(int index, IReadOnlyCollection collection) + if ((index + (collectionCount / 2)) < Count / 2) { - var collectionCount = collection.Count; - // Make room in the existing list - if (index < Count / 2) - { - // Inserting into the first half of the list - - // Move lower items down: [0, index) -> [Capacity - collectionCount, Capacity - collectionCount + index) - // This clears out the low "index" number of items, moving them "collectionCount" places down; - // after rotation, there will be a "collectionCount"-sized hole at "index". - int copyCount = index; - int writeIndex = Capacity - collectionCount; - for (int j = 0; j != copyCount; ++j) - _buffer[DequeIndexToBufferIndex(writeIndex + j)] = _buffer[DequeIndexToBufferIndex(j)]; - - // Rotate to the new view - PreDecrement(collectionCount); - } - else - { - // Inserting into the second half of the list + // Removing from first half of list - // Move higher items up: [index, count) -> [index + collectionCount, collectionCount + count) - int copyCount = Count - index; - int writeIndex = index + collectionCount; - for (int j = copyCount - 1; j != -1; --j) - _buffer[DequeIndexToBufferIndex(writeIndex + j)] = _buffer[DequeIndexToBufferIndex(index + j)]; - } - - // Copy new items into place - int i = index; - foreach (T item in collection) - { - _buffer[DequeIndexToBufferIndex(i)] = item; - ++i; - } + // Move lower items up: [0, index) -> [collectionCount, collectionCount + index) + int copyCount = index; + int writeIndex = collectionCount; + for (int j = copyCount - 1; j != -1; --j) + _buffer[DequeIndexToBufferIndex(writeIndex + j)] = _buffer[DequeIndexToBufferIndex(j)]; - // Adjust valid count - Count += collectionCount; + // Rotate to new view + PostIncrement(collectionCount); } - - /// - /// Removes a range of elements from the view. - /// - /// The index into the view at which the range begins. - /// The number of elements in the range. This must be greater than 0 and less than or equal to . - private void DoRemoveRange(int index, int collectionCount) + else { - if (index == 0) - { - // Removing from the beginning: rotate to the new view - PostIncrement(collectionCount); - Count -= collectionCount; - return; - } - else if (index == Count - collectionCount) - { - // Removing from the ending: trim the existing view - Count -= collectionCount; - return; - } + // Removing from second half of list - if ((index + (collectionCount / 2)) < Count / 2) - { - // Removing from first half of list + // Move higher items down: [index + collectionCount, count) -> [index, count - collectionCount) + int copyCount = Count - collectionCount - index; + int readIndex = index + collectionCount; + for (int j = 0; j != copyCount; ++j) + _buffer[DequeIndexToBufferIndex(index + j)] = _buffer[DequeIndexToBufferIndex(readIndex + j)]; + } - // Move lower items up: [0, index) -> [collectionCount, collectionCount + index) - int copyCount = index; - int writeIndex = collectionCount; - for (int j = copyCount - 1; j != -1; --j) - _buffer[DequeIndexToBufferIndex(writeIndex + j)] = _buffer[DequeIndexToBufferIndex(j)]; + // Adjust valid count + Count -= collectionCount; + } - // Rotate to new view - PostIncrement(collectionCount); - } - else - { - // Removing from second half of list + /// + /// Doubles the capacity if necessary to make room for one more element. When this method returns, is false. + /// + private void EnsureCapacityForOneElement() + { + if (IsFull) + { + Capacity = (Capacity == 0) ? 1 : Capacity * 2; + } + } - // Move higher items down: [index + collectionCount, count) -> [index, count - collectionCount) - int copyCount = Count - collectionCount - index; - int readIndex = index + collectionCount; - for (int j = 0; j != copyCount; ++j) - _buffer[DequeIndexToBufferIndex(index + j)] = _buffer[DequeIndexToBufferIndex(readIndex + j)]; - } + /// + /// Inserts a single element at the back of this deque. + /// + /// The element to insert. + public void AddToBack(T value) + { + EnsureCapacityForOneElement(); + DoAddToBack(value); + } - // Adjust valid count - Count -= collectionCount; - } + /// + /// Inserts a single element at the front of this deque. + /// + /// The element to insert. + public void AddToFront(T value) + { + EnsureCapacityForOneElement(); + DoAddToFront(value); + } - /// - /// Doubles the capacity if necessary to make room for one more element. When this method returns, is false. - /// - private void EnsureCapacityForOneElement() - { - if (IsFull) - { - Capacity = (Capacity == 0) ? 1 : Capacity * 2; - } - } + /// + /// Inserts a collection of elements into this deque. + /// + /// The index at which the collection is inserted. + /// The collection of elements to insert. + /// is not a valid index to an insertion point for the source. + public void InsertRange(int index, IEnumerable collection) + { + CheckNewIndexArgument(Count, index); + var source = CollectionHelpers.ReifyCollection(collection); + int collectionCount = source.Count; - /// - /// Inserts a single element at the back of this deque. - /// - /// The element to insert. - public void AddToBack(T value) + // Overflow-safe check for "Count + collectionCount > Capacity" + if (collectionCount > Capacity - Count) { - EnsureCapacityForOneElement(); - DoAddToBack(value); + Capacity = checked(Count + collectionCount); } - /// - /// Inserts a single element at the front of this deque. - /// - /// The element to insert. - public void AddToFront(T value) + if (collectionCount == 0) { - EnsureCapacityForOneElement(); - DoAddToFront(value); + return; } - /// - /// Inserts a collection of elements into this deque. - /// - /// The index at which the collection is inserted. - /// The collection of elements to insert. - /// is not a valid index to an insertion point for the source. - public void InsertRange(int index, IEnumerable collection) - { - CheckNewIndexArgument(Count, index); - var source = CollectionHelpers.ReifyCollection(collection); - int collectionCount = source.Count; - - // Overflow-safe check for "Count + collectionCount > Capacity" - if (collectionCount > Capacity - Count) - { - Capacity = checked(Count + collectionCount); - } + DoInsertRange(index, source); + } - if (collectionCount == 0) - { - return; - } + /// + /// Removes a range of elements from this deque. + /// + /// The index into the deque at which the range begins. + /// The number of elements to remove. + /// Either or is less than 0. + /// The range [, + ) is not within the range [0, ). + public void RemoveRange(int offset, int count) + { + CheckRangeArguments(Count, offset, count); - DoInsertRange(index, source); + if (count == 0) + { + return; } - /// - /// Removes a range of elements from this deque. - /// - /// The index into the deque at which the range begins. - /// The number of elements to remove. - /// Either or is less than 0. - /// The range [, + ) is not within the range [0, ). - public void RemoveRange(int offset, int count) - { - CheckRangeArguments(Count, offset, count); + DoRemoveRange(offset, count); + } - if (count == 0) - { - return; - } + /// + /// Removes and returns the last element of this deque. + /// + /// The former last element. + /// The deque is empty. + public T RemoveFromBack() + { + if (IsEmpty) + throw new InvalidOperationException("The deque is empty"); - DoRemoveRange(offset, count); - } + return DoRemoveFromBack(); + } - /// - /// Removes and returns the last element of this deque. - /// - /// The former last element. - /// The deque is empty. - public T RemoveFromBack() - { - if (IsEmpty) - throw new InvalidOperationException("The deque is empty"); + /// + /// Removes and returns the first element of this deque. + /// + /// The former first element. + /// The deque is empty. + public T RemoveFromFront() + { + if (IsEmpty) + throw new InvalidOperationException("The deque is empty"); - return DoRemoveFromBack(); - } + return DoRemoveFromFront(); + } - /// - /// Removes and returns the first element of this deque. - /// - /// The former first element. - /// The deque is empty. - public T RemoveFromFront() - { - if (IsEmpty) - throw new InvalidOperationException("The deque is empty"); + /// + /// Removes all items from this deque. + /// + public void Clear() + { + _offset = 0; + Count = 0; + } - return DoRemoveFromFront(); - } + /// + /// Creates and returns a new array containing the elements in this deque. + /// + public T[] ToArray() + { + var result = new T[Count]; + ((ICollection)this).CopyTo(result, 0); + return result; + } - /// - /// Removes all items from this deque. - /// - public void Clear() - { - _offset = 0; - Count = 0; - } + [DebuggerNonUserCode] + private sealed class DebugView + { + private readonly Deque deque; - /// - /// Creates and returns a new array containing the elements in this deque. - /// - public T[] ToArray() + public DebugView(Deque deque) { - var result = new T[Count]; - ((ICollection)this).CopyTo(result, 0); - return result; + this.deque = deque; } - [DebuggerNonUserCode] - private sealed class DebugView + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public T[] Items { - private readonly Deque deque; - - public DebugView(Deque deque) - { - this.deque = deque; - } - - [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] - public T[] Items + get { - get - { - return deque.ToArray(); - } + return deque.ToArray(); } } } diff --git a/src/Foundatio/Nito.Disposables/AnonymousDisposable.cs b/src/Foundatio/Nito.Disposables/AnonymousDisposable.cs index 99e3148bc..d4c5f60e3 100644 --- a/src/Foundatio/Nito.Disposables/AnonymousDisposable.cs +++ b/src/Foundatio/Nito.Disposables/AnonymousDisposable.cs @@ -1,45 +1,44 @@ using System; -namespace Foundatio.Disposables +namespace Foundatio.Disposables; + +/// +/// A disposable that executes a delegate when disposed. +/// +public sealed class AnonymousDisposable : SingleDisposable { /// - /// A disposable that executes a delegate when disposed. + /// Creates a new disposable that executes when disposed. /// - public sealed class AnonymousDisposable : SingleDisposable + /// The delegate to execute when disposed. If this is null, then this instance does nothing when it is disposed. + public AnonymousDisposable(Action dispose) + : base(dispose) { - /// - /// Creates a new disposable that executes when disposed. - /// - /// The delegate to execute when disposed. If this is null, then this instance does nothing when it is disposed. - public AnonymousDisposable(Action dispose) - : base(dispose) - { - } - - /// - protected override void Dispose(Action context) => context?.Invoke(); + } - /// - /// Adds a delegate to be executed when this instance is disposed. If this instance is already disposed or disposing, then is executed immediately. - /// If this method is called multiple times concurrently at the same time this instance is disposed, then the different arguments may be disposed concurrently. - /// - /// The delegate to add. May be null to indicate no additional action. - public void Add(Action dispose) - { - if (dispose == null) - return; - if (TryUpdateContext(x => x + dispose)) - return; + /// + protected override void Dispose(Action context) => context?.Invoke(); - // Wait for our disposal to complete; then call the additional delegate. - Dispose(); - dispose(); - } + /// + /// Adds a delegate to be executed when this instance is disposed. If this instance is already disposed or disposing, then is executed immediately. + /// If this method is called multiple times concurrently at the same time this instance is disposed, then the different arguments may be disposed concurrently. + /// + /// The delegate to add. May be null to indicate no additional action. + public void Add(Action dispose) + { + if (dispose == null) + return; + if (TryUpdateContext(x => x + dispose)) + return; - /// - /// Creates a new disposable that executes when disposed. - /// - /// The delegate to execute when disposed. If this is null, then this instance does nothing when it is disposed. - public static AnonymousDisposable Create(Action dispose) => new(dispose); + // Wait for our disposal to complete; then call the additional delegate. + Dispose(); + dispose(); } + + /// + /// Creates a new disposable that executes when disposed. + /// + /// The delegate to execute when disposed. If this is null, then this instance does nothing when it is disposed. + public static AnonymousDisposable Create(Action dispose) => new(dispose); } diff --git a/src/Foundatio/Nito.Disposables/Internals/BoundAction.cs b/src/Foundatio/Nito.Disposables/Internals/BoundAction.cs index 7a729636d..0f1591aed 100644 --- a/src/Foundatio/Nito.Disposables/Internals/BoundAction.cs +++ b/src/Foundatio/Nito.Disposables/Internals/BoundAction.cs @@ -3,86 +3,85 @@ using System.Linq; using System.Threading; -namespace Foundatio.Disposables.Internals +namespace Foundatio.Disposables.Internals; + +/// +/// A field containing a bound action. +/// +/// The type of context for the action. +public sealed class BoundActionField { + private BoundAction _field; + /// - /// A field containing a bound action. + /// Initializes the field with the specified action and context. /// - /// The type of context for the action. - public sealed class BoundActionField + /// The action delegate. + /// The context. + public BoundActionField(Action action, T context) { - private BoundAction _field; + _field = new BoundAction(action, context); + } - /// - /// Initializes the field with the specified action and context. - /// - /// The action delegate. - /// The context. - public BoundActionField(Action action, T context) - { - _field = new BoundAction(action, context); - } + /// + /// Whether the field is empty. + /// + public bool IsEmpty => Interlocked.CompareExchange(ref _field, null, null) == null; - /// - /// Whether the field is empty. - /// - public bool IsEmpty => Interlocked.CompareExchange(ref _field, null, null) == null; + /// + /// Atomically retrieves the bound action from the field and sets the field to null. May return null. + /// + public IBoundAction TryGetAndUnset() + { + return Interlocked.Exchange(ref _field, null); + } - /// - /// Atomically retrieves the bound action from the field and sets the field to null. May return null. - /// - public IBoundAction TryGetAndUnset() + /// + /// Attempts to update the context of the bound action stored in the field. Returns false if the field is null. + /// + /// The function used to update an existing context. This may be called more than once if more than one thread attempts to simultaneously update the context. + public bool TryUpdateContext(Func contextUpdater) + { + while (true) { - return Interlocked.Exchange(ref _field, null); + var original = Interlocked.CompareExchange(ref _field, _field, _field); + if (original == null) + return false; + var updatedContext = new BoundAction(original, contextUpdater); + var result = Interlocked.CompareExchange(ref _field, updatedContext, original); + if (ReferenceEquals(original, result)) + return true; } + } + /// + /// An action delegate bound with its context. + /// + public interface IBoundAction + { /// - /// Attempts to update the context of the bound action stored in the field. Returns false if the field is null. + /// Executes the action. This should only be done after the bound action is retrieved from a field by . /// - /// The function used to update an existing context. This may be called more than once if more than one thread attempts to simultaneously update the context. - public bool TryUpdateContext(Func contextUpdater) - { - while (true) - { - var original = Interlocked.CompareExchange(ref _field, _field, _field); - if (original == null) - return false; - var updatedContext = new BoundAction(original, contextUpdater); - var result = Interlocked.CompareExchange(ref _field, updatedContext, original); - if (ReferenceEquals(original, result)) - return true; - } - } + void Invoke(); + } - /// - /// An action delegate bound with its context. - /// - public interface IBoundAction + private sealed class BoundAction : IBoundAction + { + private readonly Action _action; + private readonly T _context; + + public BoundAction(Action action, T context) { - /// - /// Executes the action. This should only be done after the bound action is retrieved from a field by . - /// - void Invoke(); + _action = action; + _context = context; } - private sealed class BoundAction : IBoundAction + public BoundAction(BoundAction originalBoundAction, Func contextUpdater) { - private readonly Action _action; - private readonly T _context; - - public BoundAction(Action action, T context) - { - _action = action; - _context = context; - } - - public BoundAction(BoundAction originalBoundAction, Func contextUpdater) - { - _action = originalBoundAction._action; - _context = contextUpdater(originalBoundAction._context); - } - - public void Invoke() => _action?.Invoke(_context); + _action = originalBoundAction._action; + _context = contextUpdater(originalBoundAction._context); } + + public void Invoke() => _action?.Invoke(_context); } } diff --git a/src/Foundatio/Nito.Disposables/SingleDisposable.cs b/src/Foundatio/Nito.Disposables/SingleDisposable.cs index f88c9ee7b..abee47905 100644 --- a/src/Foundatio/Nito.Disposables/SingleDisposable.cs +++ b/src/Foundatio/Nito.Disposables/SingleDisposable.cs @@ -3,82 +3,81 @@ using System.Threading.Tasks; using Foundatio.Disposables.Internals; -namespace Foundatio.Disposables +namespace Foundatio.Disposables; + +/// +/// A base class for disposables that need exactly-once semantics in a thread-safe way. All disposals of this instance block until the disposal is complete. +/// +/// The type of "context" for the derived disposable. Since the context should not be modified, strongly consider making this an immutable type. +/// +/// If is called multiple times, only the first call will execute the disposal code. Other calls to will wait for the disposal to complete. +/// +public abstract class SingleDisposable : IDisposable { /// - /// A base class for disposables that need exactly-once semantics in a thread-safe way. All disposals of this instance block until the disposal is complete. + /// The context. This is never null. This is empty if this instance has already been disposed (or is being disposed). /// - /// The type of "context" for the derived disposable. Since the context should not be modified, strongly consider making this an immutable type. - /// - /// If is called multiple times, only the first call will execute the disposal code. Other calls to will wait for the disposal to complete. - /// - public abstract class SingleDisposable : IDisposable - { - /// - /// The context. This is never null. This is empty if this instance has already been disposed (or is being disposed). - /// - private readonly BoundActionField _context; + private readonly BoundActionField _context; - private readonly TaskCompletionSource _tcs = new(); + private readonly TaskCompletionSource _tcs = new(); - /// - /// Creates a disposable for the specified context. - /// - /// The context passed to . May be null. - protected SingleDisposable(T context) - { - _context = new BoundActionField(Dispose, context); - } + /// + /// Creates a disposable for the specified context. + /// + /// The context passed to . May be null. + protected SingleDisposable(T context) + { + _context = new BoundActionField(Dispose, context); + } - /// - /// Whether this instance is currently disposing or has been disposed. - /// - public bool IsDisposeStarted => _context.IsEmpty; + /// + /// Whether this instance is currently disposing or has been disposed. + /// + public bool IsDisposeStarted => _context.IsEmpty; - /// - /// Whether this instance is disposed (finished disposing). - /// - public bool IsDisposed => _tcs.Task.IsCompleted; + /// + /// Whether this instance is disposed (finished disposing). + /// + public bool IsDisposed => _tcs.Task.IsCompleted; - /// - /// Whether this instance is currently disposing, but not finished yet. - /// - public bool IsDisposing => IsDisposeStarted && !IsDisposed; + /// + /// Whether this instance is currently disposing, but not finished yet. + /// + public bool IsDisposing => IsDisposeStarted && !IsDisposed; - /// - /// The actul disposal method, called only once from . - /// - /// The context for the disposal operation. - protected abstract void Dispose(T context); + /// + /// The actul disposal method, called only once from . + /// + /// The context for the disposal operation. + protected abstract void Dispose(T context); - /// - /// Disposes this instance. - /// - /// - /// If is called multiple times, only the first call will execute the disposal code. Other calls to will wait for the disposal to complete. - /// - public void Dispose() + /// + /// Disposes this instance. + /// + /// + /// If is called multiple times, only the first call will execute the disposal code. Other calls to will wait for the disposal to complete. + /// + public void Dispose() + { + var context = _context.TryGetAndUnset(); + if (context == null) { - var context = _context.TryGetAndUnset(); - if (context == null) - { - _tcs.Task.GetAwaiter().GetResult(); - return; - } - try - { - context.Invoke(); - } - finally - { - _tcs.TrySetResult(null!); - } + _tcs.Task.GetAwaiter().GetResult(); + return; + } + try + { + context.Invoke(); + } + finally + { + _tcs.TrySetResult(null!); } - - /// - /// Attempts to update the stored context. This method returns false if this instance has already been disposed (or is being disposed). - /// - /// The function used to update an existing context. This may be called more than once if more than one thread attempts to simultaneously update the context. - protected bool TryUpdateContext(Func contextUpdater) => _context.TryUpdateContext(contextUpdater); } + + /// + /// Attempts to update the stored context. This method returns false if this instance has already been disposed (or is being disposed). + /// + /// The function used to update an existing context. This may be called more than once if more than one thread attempts to simultaneously update the context. + protected bool TryUpdateContext(Func contextUpdater) => _context.TryUpdateContext(contextUpdater); } diff --git a/src/Foundatio/Queues/DuplicateDetectionQueueBehavior.cs b/src/Foundatio/Queues/DuplicateDetectionQueueBehavior.cs index 5383d7acb..12821b31a 100644 --- a/src/Foundatio/Queues/DuplicateDetectionQueueBehavior.cs +++ b/src/Foundatio/Queues/DuplicateDetectionQueueBehavior.cs @@ -3,54 +3,53 @@ using Foundatio.Caching; using Microsoft.Extensions.Logging; -namespace Foundatio.Queues +namespace Foundatio.Queues; + +public class DuplicateDetectionQueueBehavior : QueueBehaviorBase where T : class { - public class DuplicateDetectionQueueBehavior : QueueBehaviorBase where T : class + private readonly ICacheClient _cacheClient; + private readonly ILoggerFactory _loggerFactory; + private readonly TimeSpan _detectionWindow; + + public DuplicateDetectionQueueBehavior(ICacheClient cacheClient, ILoggerFactory loggerFactory, TimeSpan? detectionWindow = null) { - private readonly ICacheClient _cacheClient; - private readonly ILoggerFactory _loggerFactory; - private readonly TimeSpan _detectionWindow; + _cacheClient = cacheClient; + _loggerFactory = loggerFactory; + _detectionWindow = detectionWindow ?? TimeSpan.FromMinutes(10); + } - public DuplicateDetectionQueueBehavior(ICacheClient cacheClient, ILoggerFactory loggerFactory, TimeSpan? detectionWindow = null) - { - _cacheClient = cacheClient; - _loggerFactory = loggerFactory; - _detectionWindow = detectionWindow ?? TimeSpan.FromMinutes(10); - } + protected override async Task OnEnqueuing(object sender, EnqueuingEventArgs enqueuingEventArgs) + { + string uniqueIdentifier = GetUniqueIdentifier(enqueuingEventArgs.Data); + if (String.IsNullOrEmpty(uniqueIdentifier)) + return; - protected override async Task OnEnqueuing(object sender, EnqueuingEventArgs enqueuingEventArgs) + bool success = await _cacheClient.AddAsync(uniqueIdentifier, true, _detectionWindow); + if (!success) { - string uniqueIdentifier = GetUniqueIdentifier(enqueuingEventArgs.Data); - if (String.IsNullOrEmpty(uniqueIdentifier)) - return; - - bool success = await _cacheClient.AddAsync(uniqueIdentifier, true, _detectionWindow); - if (!success) - { - var logger = _loggerFactory.CreateLogger(); - logger.LogInformation("Discarding queue entry due to duplicate {UniqueIdentifier}", uniqueIdentifier); - enqueuingEventArgs.Cancel = true; - } + var logger = _loggerFactory.CreateLogger(); + logger.LogInformation("Discarding queue entry due to duplicate {UniqueIdentifier}", uniqueIdentifier); + enqueuingEventArgs.Cancel = true; } + } - protected override async Task OnDequeued(object sender, DequeuedEventArgs dequeuedEventArgs) - { - string uniqueIdentifier = GetUniqueIdentifier(dequeuedEventArgs.Entry.Value); - if (String.IsNullOrEmpty(uniqueIdentifier)) - return; - - await _cacheClient.RemoveAsync(uniqueIdentifier); - } + protected override async Task OnDequeued(object sender, DequeuedEventArgs dequeuedEventArgs) + { + string uniqueIdentifier = GetUniqueIdentifier(dequeuedEventArgs.Entry.Value); + if (String.IsNullOrEmpty(uniqueIdentifier)) + return; - private string GetUniqueIdentifier(T data) - { - var haveUniqueIdentifier = data as IHaveUniqueIdentifier; - return haveUniqueIdentifier?.UniqueIdentifier; - } + await _cacheClient.RemoveAsync(uniqueIdentifier); } - public interface IHaveUniqueIdentifier + private string GetUniqueIdentifier(T data) { - string UniqueIdentifier { get; } + var haveUniqueIdentifier = data as IHaveUniqueIdentifier; + return haveUniqueIdentifier?.UniqueIdentifier; } } + +public interface IHaveUniqueIdentifier +{ + string UniqueIdentifier { get; } +} diff --git a/src/Foundatio/Queues/IQueue.cs b/src/Foundatio/Queues/IQueue.cs index 52785c1fc..1ad6d6841 100644 --- a/src/Foundatio/Queues/IQueue.cs +++ b/src/Foundatio/Queues/IQueue.cs @@ -7,110 +7,109 @@ using Foundatio.Serializer; using Foundatio.Utility; -namespace Foundatio.Queues +namespace Foundatio.Queues; + +public interface IQueue : IQueue where T : class { - public interface IQueue : IQueue where T : class - { - AsyncEvent> Enqueuing { get; } - AsyncEvent> Enqueued { get; } - AsyncEvent> Dequeued { get; } - AsyncEvent> LockRenewed { get; } - AsyncEvent> Completed { get; } - AsyncEvent> Abandoned { get; } + AsyncEvent> Enqueuing { get; } + AsyncEvent> Enqueued { get; } + AsyncEvent> Dequeued { get; } + AsyncEvent> LockRenewed { get; } + AsyncEvent> Completed { get; } + AsyncEvent> Abandoned { get; } - void AttachBehavior(IQueueBehavior behavior); - Task EnqueueAsync(T data, QueueEntryOptions options = null); - Task> DequeueAsync(CancellationToken cancellationToken); - Task> DequeueAsync(TimeSpan? timeout = null); - Task RenewLockAsync(IQueueEntry queueEntry); - Task CompleteAsync(IQueueEntry queueEntry); - Task AbandonAsync(IQueueEntry queueEntry); - Task> GetDeadletterItemsAsync(CancellationToken cancellationToken = default); - /// - /// Asynchronously dequeues entries in the background. - /// - /// - /// Function called on entry dequeued. - /// - /// - /// True to call after the is run, - /// defaults to false. - /// - /// - /// The token used to cancel the background worker. - /// - Task StartWorkingAsync(Func, CancellationToken, Task> handler, bool autoComplete = false, CancellationToken cancellationToken = default); - } + void AttachBehavior(IQueueBehavior behavior); + Task EnqueueAsync(T data, QueueEntryOptions options = null); + Task> DequeueAsync(CancellationToken cancellationToken); + Task> DequeueAsync(TimeSpan? timeout = null); + Task RenewLockAsync(IQueueEntry queueEntry); + Task CompleteAsync(IQueueEntry queueEntry); + Task AbandonAsync(IQueueEntry queueEntry); + Task> GetDeadletterItemsAsync(CancellationToken cancellationToken = default); + /// + /// Asynchronously dequeues entries in the background. + /// + /// + /// Function called on entry dequeued. + /// + /// + /// True to call after the is run, + /// defaults to false. + /// + /// + /// The token used to cancel the background worker. + /// + Task StartWorkingAsync(Func, CancellationToken, Task> handler, bool autoComplete = false, CancellationToken cancellationToken = default); +} - public interface IQueue : IHaveSerializer, IDisposable - { - Task GetQueueStatsAsync(); - Task DeleteQueueAsync(); - string QueueId { get; } - } +public interface IQueue : IHaveSerializer, IDisposable +{ + Task GetQueueStatsAsync(); + Task DeleteQueueAsync(); + string QueueId { get; } +} - public static class QueueExtensions - { - public static Task StartWorkingAsync(this IQueue queue, Func, Task> handler, bool autoComplete = false, CancellationToken cancellationToken = default) where T : class - => queue.StartWorkingAsync((entry, token) => handler(entry), autoComplete, cancellationToken); - } +public static class QueueExtensions +{ + public static Task StartWorkingAsync(this IQueue queue, Func, Task> handler, bool autoComplete = false, CancellationToken cancellationToken = default) where T : class + => queue.StartWorkingAsync((entry, token) => handler(entry), autoComplete, cancellationToken); +} - [DebuggerDisplay("Queued={Queued}, Working={Working}, Deadletter={Deadletter}, Enqueued={Enqueued}, Dequeued={Dequeued}, Completed={Completed}, Abandoned={Abandoned}, Errors={Errors}, Timeouts={Timeouts}")] - public class QueueStats - { - public long Queued { get; set; } - public long Working { get; set; } - public long Deadletter { get; set; } - public long Enqueued { get; set; } - public long Dequeued { get; set; } - public long Completed { get; set; } - public long Abandoned { get; set; } - public long Errors { get; set; } - public long Timeouts { get; set; } - } +[DebuggerDisplay("Queued={Queued}, Working={Working}, Deadletter={Deadletter}, Enqueued={Enqueued}, Dequeued={Dequeued}, Completed={Completed}, Abandoned={Abandoned}, Errors={Errors}, Timeouts={Timeouts}")] +public class QueueStats +{ + public long Queued { get; set; } + public long Working { get; set; } + public long Deadletter { get; set; } + public long Enqueued { get; set; } + public long Dequeued { get; set; } + public long Completed { get; set; } + public long Abandoned { get; set; } + public long Errors { get; set; } + public long Timeouts { get; set; } +} - public class QueueEntryOptions - { - public string UniqueId { get; set; } - public string CorrelationId { get; set; } - public TimeSpan? DeliveryDelay { get; set; } - public IDictionary Properties { get; set; } = new Dictionary(); - } +public class QueueEntryOptions +{ + public string UniqueId { get; set; } + public string CorrelationId { get; set; } + public TimeSpan? DeliveryDelay { get; set; } + public IDictionary Properties { get; set; } = new Dictionary(); +} - public class EnqueuingEventArgs : CancelEventArgs where T : class - { - public IQueue Queue { get; set; } - public T Data { get; set; } - public QueueEntryOptions Options { get; set; } - } +public class EnqueuingEventArgs : CancelEventArgs where T : class +{ + public IQueue Queue { get; set; } + public T Data { get; set; } + public QueueEntryOptions Options { get; set; } +} - public class EnqueuedEventArgs : EventArgs where T : class - { - public IQueue Queue { get; set; } - public IQueueEntry Entry { get; set; } - } +public class EnqueuedEventArgs : EventArgs where T : class +{ + public IQueue Queue { get; set; } + public IQueueEntry Entry { get; set; } +} - public class DequeuedEventArgs : EventArgs where T : class - { - public IQueue Queue { get; set; } - public IQueueEntry Entry { get; set; } - } +public class DequeuedEventArgs : EventArgs where T : class +{ + public IQueue Queue { get; set; } + public IQueueEntry Entry { get; set; } +} - public class LockRenewedEventArgs : EventArgs where T : class - { - public IQueue Queue { get; set; } - public IQueueEntry Entry { get; set; } - } +public class LockRenewedEventArgs : EventArgs where T : class +{ + public IQueue Queue { get; set; } + public IQueueEntry Entry { get; set; } +} - public class CompletedEventArgs : EventArgs where T : class - { - public IQueue Queue { get; set; } - public IQueueEntry Entry { get; set; } - } +public class CompletedEventArgs : EventArgs where T : class +{ + public IQueue Queue { get; set; } + public IQueueEntry Entry { get; set; } +} - public class AbandonedEventArgs : EventArgs where T : class - { - public IQueue Queue { get; set; } - public IQueueEntry Entry { get; set; } - } +public class AbandonedEventArgs : EventArgs where T : class +{ + public IQueue Queue { get; set; } + public IQueueEntry Entry { get; set; } } diff --git a/src/Foundatio/Queues/IQueueActivity.cs b/src/Foundatio/Queues/IQueueActivity.cs index 9effe8ce3..ea16697f5 100644 --- a/src/Foundatio/Queues/IQueueActivity.cs +++ b/src/Foundatio/Queues/IQueueActivity.cs @@ -1,10 +1,9 @@ using System; -namespace Foundatio.Queues +namespace Foundatio.Queues; + +public interface IQueueActivity { - public interface IQueueActivity - { - DateTime? LastEnqueueActivity { get; } - DateTime? LastDequeueActivity { get; } - } + DateTime? LastEnqueueActivity { get; } + DateTime? LastDequeueActivity { get; } } diff --git a/src/Foundatio/Queues/IQueueEntry.cs b/src/Foundatio/Queues/IQueueEntry.cs index 2fdfe423d..16e49d3c4 100644 --- a/src/Foundatio/Queues/IQueueEntry.cs +++ b/src/Foundatio/Queues/IQueueEntry.cs @@ -3,28 +3,27 @@ using System.Threading.Tasks; using Foundatio.Utility; -namespace Foundatio.Queues +namespace Foundatio.Queues; + +public interface IQueueEntry { - public interface IQueueEntry - { - string Id { get; } - string CorrelationId { get; } - IDictionary Properties { get; } - Type EntryType { get; } - object GetValue(); - bool IsCompleted { get; } - bool IsAbandoned { get; } - int Attempts { get; } - void MarkAbandoned(); - void MarkCompleted(); - Task RenewLockAsync(); - Task AbandonAsync(); - Task CompleteAsync(); - ValueTask DisposeAsync(); - } + string Id { get; } + string CorrelationId { get; } + IDictionary Properties { get; } + Type EntryType { get; } + object GetValue(); + bool IsCompleted { get; } + bool IsAbandoned { get; } + int Attempts { get; } + void MarkAbandoned(); + void MarkCompleted(); + Task RenewLockAsync(); + Task AbandonAsync(); + Task CompleteAsync(); + ValueTask DisposeAsync(); +} - public interface IQueueEntry : IQueueEntry where T : class - { - T Value { get; } - } +public interface IQueueEntry : IQueueEntry where T : class +{ + T Value { get; } } diff --git a/src/Foundatio/Queues/InMemoryQueue.cs b/src/Foundatio/Queues/InMemoryQueue.cs index 133b131f3..8a37aa4bb 100644 --- a/src/Foundatio/Queues/InMemoryQueue.cs +++ b/src/Foundatio/Queues/InMemoryQueue.cs @@ -10,407 +10,406 @@ using Foundatio.Utility; using Microsoft.Extensions.Logging; -namespace Foundatio.Queues +namespace Foundatio.Queues; + +public class InMemoryQueue : QueueBase> where T : class { - public class InMemoryQueue : QueueBase> where T : class + private readonly ConcurrentQueue> _queue = new(); + private readonly ConcurrentDictionary> _dequeued = new(); + private readonly ConcurrentQueue> _deadletterQueue = new(); + private readonly ConcurrentQueue> _completedQueue = new(); + private readonly AsyncAutoResetEvent _autoResetEvent = new(); + + private int _enqueuedCount; + private int _dequeuedCount; + private int _completedCount; + private int _abandonedCount; + private int _workerErrorCount; + private int _workerItemTimeoutCount; + + public InMemoryQueue() : this(o => o) { } + + public InMemoryQueue(InMemoryQueueOptions options) : base(options) { - private readonly ConcurrentQueue> _queue = new(); - private readonly ConcurrentDictionary> _dequeued = new(); - private readonly ConcurrentQueue> _deadletterQueue = new(); - private readonly ConcurrentQueue> _completedQueue = new(); - private readonly AsyncAutoResetEvent _autoResetEvent = new(); - - private int _enqueuedCount; - private int _dequeuedCount; - private int _completedCount; - private int _abandonedCount; - private int _workerErrorCount; - private int _workerItemTimeoutCount; - - public InMemoryQueue() : this(o => o) { } - - public InMemoryQueue(InMemoryQueueOptions options) : base(options) - { - InitializeMaintenance(); - } + InitializeMaintenance(); + } - public InMemoryQueue(Builder, InMemoryQueueOptions> config) - : this(config(new InMemoryQueueOptionsBuilder()).Build()) { } + public InMemoryQueue(Builder, InMemoryQueueOptions> config) + : this(config(new InMemoryQueueOptionsBuilder()).Build()) { } - protected override Task EnsureQueueCreatedAsync(CancellationToken cancellationToken = default) - { - return Task.CompletedTask; - } + protected override Task EnsureQueueCreatedAsync(CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } - protected override Task GetQueueStatsImplAsync() - { - return Task.FromResult(GetMetricsQueueStats()); - } + protected override Task GetQueueStatsImplAsync() + { + return Task.FromResult(GetMetricsQueueStats()); + } - protected override QueueStats GetMetricsQueueStats() + protected override QueueStats GetMetricsQueueStats() + { + return new QueueStats { - return new QueueStats - { - Queued = _queue.Count, - Working = _dequeued.Count, - Deadletter = _deadletterQueue.Count, - Enqueued = _enqueuedCount, - Dequeued = _dequeuedCount, - Completed = _completedCount, - Abandoned = _abandonedCount, - Errors = _workerErrorCount, - Timeouts = _workerItemTimeoutCount - }; - } + Queued = _queue.Count, + Working = _dequeued.Count, + Deadletter = _deadletterQueue.Count, + Enqueued = _enqueuedCount, + Dequeued = _dequeuedCount, + Completed = _completedCount, + Abandoned = _abandonedCount, + Errors = _workerErrorCount, + Timeouts = _workerItemTimeoutCount + }; + } - public IReadOnlyCollection> GetEntries() - { - return new ReadOnlyCollection>(_queue.ToList()); - } + public IReadOnlyCollection> GetEntries() + { + return new ReadOnlyCollection>(_queue.ToList()); + } - public IReadOnlyCollection> GetDequeuedEntries() - { - return new ReadOnlyCollection>(_dequeued.Values.ToList()); - } + public IReadOnlyCollection> GetDequeuedEntries() + { + return new ReadOnlyCollection>(_dequeued.Values.ToList()); + } - public IReadOnlyCollection> GetCompletedEntries() - { - return new ReadOnlyCollection>(_completedQueue.ToList()); - } + public IReadOnlyCollection> GetCompletedEntries() + { + return new ReadOnlyCollection>(_completedQueue.ToList()); + } - public IReadOnlyCollection> GetDeadletterEntries() - { - return new ReadOnlyCollection>(_deadletterQueue.ToList()); - } + public IReadOnlyCollection> GetDeadletterEntries() + { + return new ReadOnlyCollection>(_deadletterQueue.ToList()); + } - protected override async Task EnqueueImplAsync(T data, QueueEntryOptions options) - { - string id = Guid.NewGuid().ToString("N"); - _logger.LogTrace("Queue {Name} enqueue item: {Id}", _options.Name, id); + protected override async Task EnqueueImplAsync(T data, QueueEntryOptions options) + { + string id = Guid.NewGuid().ToString("N"); + _logger.LogTrace("Queue {Name} enqueue item: {Id}", _options.Name, id); - if (!await OnEnqueuingAsync(data, options).AnyContext()) - return null; + if (!await OnEnqueuingAsync(data, options).AnyContext()) + return null; - var entry = new QueueEntry(id, options?.CorrelationId, data.DeepClone(), this, SystemClock.UtcNow, 0); - entry.Properties.AddRange(options?.Properties); + var entry = new QueueEntry(id, options?.CorrelationId, data.DeepClone(), this, SystemClock.UtcNow, 0); + entry.Properties.AddRange(options?.Properties); - Interlocked.Increment(ref _enqueuedCount); + Interlocked.Increment(ref _enqueuedCount); - if (options?.DeliveryDelay != null && options.DeliveryDelay.Value > TimeSpan.Zero) + if (options?.DeliveryDelay != null && options.DeliveryDelay.Value > TimeSpan.Zero) + { + _ = Run.DelayedAsync(options.DeliveryDelay.Value, async () => { - _ = Run.DelayedAsync(options.DeliveryDelay.Value, async () => - { - _queue.Enqueue(entry); - _logger.LogTrace("Enqueue: Set Event"); + _queue.Enqueue(entry); + _logger.LogTrace("Enqueue: Set Event"); - _autoResetEvent.Set(); + _autoResetEvent.Set(); - await OnEnqueuedAsync(entry).AnyContext(); - _logger.LogTrace("Enqueue done"); - }, _queueDisposedCancellationTokenSource.Token); - return id; - } + await OnEnqueuedAsync(entry).AnyContext(); + _logger.LogTrace("Enqueue done"); + }, _queueDisposedCancellationTokenSource.Token); + return id; + } - _queue.Enqueue(entry); - _logger.LogTrace("Enqueue: Set Event"); + _queue.Enqueue(entry); + _logger.LogTrace("Enqueue: Set Event"); - _autoResetEvent.Set(); + _autoResetEvent.Set(); - await OnEnqueuedAsync(entry).AnyContext(); - _logger.LogTrace("Enqueue done"); + await OnEnqueuedAsync(entry).AnyContext(); + _logger.LogTrace("Enqueue done"); - return id; - } + return id; + } - private readonly List _workers = new(); + private readonly List _workers = new(); - protected override void StartWorkingImpl(Func, CancellationToken, Task> handler, bool autoComplete, CancellationToken cancellationToken) - { - if (handler == null) - throw new ArgumentNullException(nameof(handler)); + protected override void StartWorkingImpl(Func, CancellationToken, Task> handler, bool autoComplete, CancellationToken cancellationToken) + { + if (handler == null) + throw new ArgumentNullException(nameof(handler)); - _logger.LogTrace("Queue {Name} start working", _options.Name); + _logger.LogTrace("Queue {Name} start working", _options.Name); - _workers.Add(Task.Run(async () => + _workers.Add(Task.Run(async () => + { + using var linkedCancellationToken = GetLinkedDisposableCancellationTokenSource(cancellationToken); + _logger.LogTrace("WorkerLoop Start {Name}", _options.Name); + + while (!linkedCancellationToken.IsCancellationRequested) { - using var linkedCancellationToken = GetLinkedDisposableCancellationTokenSource(cancellationToken); - _logger.LogTrace("WorkerLoop Start {Name}", _options.Name); + _logger.LogTrace("WorkerLoop Signaled {Name}", _options.Name); - while (!linkedCancellationToken.IsCancellationRequested) + IQueueEntry queueEntry = null; + try { - _logger.LogTrace("WorkerLoop Signaled {Name}", _options.Name); + queueEntry = await DequeueImplAsync(linkedCancellationToken.Token).AnyContext(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error on Dequeue: {Message}", ex.Message); + } - IQueueEntry queueEntry = null; - try - { - queueEntry = await DequeueImplAsync(linkedCancellationToken.Token).AnyContext(); - } - catch (Exception ex) + if (linkedCancellationToken.IsCancellationRequested || queueEntry == null) + return; + + try + { + await handler(queueEntry, linkedCancellationToken.Token).AnyContext(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Worker error: {Message}", ex.Message); + + if (!queueEntry.IsAbandoned && !queueEntry.IsCompleted) { - _logger.LogError(ex, "Error on Dequeue: {Message}", ex.Message); + try + { + await Run.WithRetriesAsync(() => queueEntry.AbandonAsync(), 3, TimeSpan.Zero, cancellationToken).AnyContext(); + } + catch (Exception abandonEx) + { + _logger.LogError(abandonEx, "Worker error abandoning queue entry: {Message}", abandonEx.Message); + } } - if (linkedCancellationToken.IsCancellationRequested || queueEntry == null) - return; + Interlocked.Increment(ref _workerErrorCount); + } + if (autoComplete && !queueEntry.IsAbandoned && !queueEntry.IsCompleted) + { try { - await handler(queueEntry, linkedCancellationToken.Token).AnyContext(); + await Run.WithRetriesAsync(() => queueEntry.CompleteAsync(), cancellationToken: linkedCancellationToken.Token, logger: _logger).AnyContext(); } catch (Exception ex) { - _logger.LogError(ex, "Worker error: {Message}", ex.Message); - - if (!queueEntry.IsAbandoned && !queueEntry.IsCompleted) - { - try - { - await Run.WithRetriesAsync(() => queueEntry.AbandonAsync(), 3, TimeSpan.Zero, cancellationToken).AnyContext(); - } - catch (Exception abandonEx) - { - _logger.LogError(abandonEx, "Worker error abandoning queue entry: {Message}", abandonEx.Message); - } - } - - Interlocked.Increment(ref _workerErrorCount); - } - - if (autoComplete && !queueEntry.IsAbandoned && !queueEntry.IsCompleted) - { - try - { - await Run.WithRetriesAsync(() => queueEntry.CompleteAsync(), cancellationToken: linkedCancellationToken.Token, logger: _logger).AnyContext(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Worker error attempting to auto complete entry: {Message}", ex.Message); - } + _logger.LogError(ex, "Worker error attempting to auto complete entry: {Message}", ex.Message); } } + } - _logger.LogTrace("Worker exiting: {Name} Cancel Requested: {IsCancellationRequested}", _options.Name, linkedCancellationToken.IsCancellationRequested); - }, GetLinkedDisposableCancellationTokenSource(cancellationToken).Token)); - } + _logger.LogTrace("Worker exiting: {Name} Cancel Requested: {IsCancellationRequested}", _options.Name, linkedCancellationToken.IsCancellationRequested); + }, GetLinkedDisposableCancellationTokenSource(cancellationToken).Token)); + } - protected override async Task> DequeueImplAsync(CancellationToken linkedCancellationToken) + protected override async Task> DequeueImplAsync(CancellationToken linkedCancellationToken) + { + _logger.LogTrace("Queue {Name} dequeuing item... Queue count: {Count}", _options.Name, _queue.Count); + + while (_queue.Count == 0 && !linkedCancellationToken.IsCancellationRequested) { - _logger.LogTrace("Queue {Name} dequeuing item... Queue count: {Count}", _options.Name, _queue.Count); + _logger.LogTrace("Waiting to dequeue item..."); + var sw = Stopwatch.StartNew(); - while (_queue.Count == 0 && !linkedCancellationToken.IsCancellationRequested) + try { - _logger.LogTrace("Waiting to dequeue item..."); - var sw = Stopwatch.StartNew(); - - try - { - using var timeoutCancellationTokenSource = new CancellationTokenSource(10000); - using var dequeueCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(linkedCancellationToken, timeoutCancellationTokenSource.Token); - await _autoResetEvent.WaitAsync(dequeueCancellationTokenSource.Token).AnyContext(); - } - catch (OperationCanceledException) { } - - sw.Stop(); - _logger.LogTrace("Waited for dequeue: {Elapsed:g}", sw.Elapsed); + using var timeoutCancellationTokenSource = new CancellationTokenSource(10000); + using var dequeueCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(linkedCancellationToken, timeoutCancellationTokenSource.Token); + await _autoResetEvent.WaitAsync(dequeueCancellationTokenSource.Token).AnyContext(); } + catch (OperationCanceledException) { } - if (_queue.Count == 0) - return null; + sw.Stop(); + _logger.LogTrace("Waited for dequeue: {Elapsed:g}", sw.Elapsed); + } - _logger.LogTrace("Dequeue: Attempt"); - if (!_queue.TryDequeue(out var entry) || entry == null) - return null; + if (_queue.Count == 0) + return null; - entry.Attempts++; - entry.DequeuedTimeUtc = SystemClock.UtcNow; + _logger.LogTrace("Dequeue: Attempt"); + if (!_queue.TryDequeue(out var entry) || entry == null) + return null; - if (!_dequeued.TryAdd(entry.Id, entry)) - throw new Exception("Unable to add item to the dequeued list"); + entry.Attempts++; + entry.DequeuedTimeUtc = SystemClock.UtcNow; - Interlocked.Increment(ref _dequeuedCount); - _logger.LogTrace("Dequeue: Got Item"); + if (!_dequeued.TryAdd(entry.Id, entry)) + throw new Exception("Unable to add item to the dequeued list"); - await entry.RenewLockAsync(); - await OnDequeuedAsync(entry).AnyContext(); - ScheduleNextMaintenance(SystemClock.UtcNow.Add(_options.WorkItemTimeout)); + Interlocked.Increment(ref _dequeuedCount); + _logger.LogTrace("Dequeue: Got Item"); - return entry; - } + await entry.RenewLockAsync(); + await OnDequeuedAsync(entry).AnyContext(); + ScheduleNextMaintenance(SystemClock.UtcNow.Add(_options.WorkItemTimeout)); - public override async Task RenewLockAsync(IQueueEntry entry) - { - _logger.LogDebug("Queue {Name} renew lock item: {Id}", _options.Name, entry.Id); + return entry; + } - if (!_dequeued.TryGetValue(entry.Id, out var targetEntry)) - return; + public override async Task RenewLockAsync(IQueueEntry entry) + { + _logger.LogDebug("Queue {Name} renew lock item: {Id}", _options.Name, entry.Id); - targetEntry.RenewedTimeUtc = SystemClock.UtcNow; + if (!_dequeued.TryGetValue(entry.Id, out var targetEntry)) + return; - await OnLockRenewedAsync(entry).AnyContext(); - _logger.LogTrace("Renew lock done: {Id}", entry.Id); - } + targetEntry.RenewedTimeUtc = SystemClock.UtcNow; - public override async Task CompleteAsync(IQueueEntry entry) - { - _logger.LogDebug("Queue {Name} complete item: {Id}", _options.Name, entry.Id); - if (entry.IsAbandoned || entry.IsCompleted) - throw new InvalidOperationException("Queue entry has already been completed or abandoned"); + await OnLockRenewedAsync(entry).AnyContext(); + _logger.LogTrace("Renew lock done: {Id}", entry.Id); + } - if (!_dequeued.TryRemove(entry.Id, out var info) || info == null) - throw new Exception("Unable to remove item from the dequeued list"); + public override async Task CompleteAsync(IQueueEntry entry) + { + _logger.LogDebug("Queue {Name} complete item: {Id}", _options.Name, entry.Id); + if (entry.IsAbandoned || entry.IsCompleted) + throw new InvalidOperationException("Queue entry has already been completed or abandoned"); - if (_options.CompletedEntryRetentionLimit > 0) - { - _completedQueue.Enqueue(info); - while (_completedQueue.Count > _options.CompletedEntryRetentionLimit) - _completedQueue.TryDequeue(out _); - } + if (!_dequeued.TryRemove(entry.Id, out var info) || info == null) + throw new Exception("Unable to remove item from the dequeued list"); - entry.MarkCompleted(); - Interlocked.Increment(ref _completedCount); - await OnCompletedAsync(entry).AnyContext(); - _logger.LogTrace("Complete done: {Id}", entry.Id); + if (_options.CompletedEntryRetentionLimit > 0) + { + _completedQueue.Enqueue(info); + while (_completedQueue.Count > _options.CompletedEntryRetentionLimit) + _completedQueue.TryDequeue(out _); } - public override async Task AbandonAsync(IQueueEntry entry) - { - _logger.LogDebug("Queue {Name}:{QueueId} abandon item: {Id}", _options.Name, QueueId, entry.Id); + entry.MarkCompleted(); + Interlocked.Increment(ref _completedCount); + await OnCompletedAsync(entry).AnyContext(); + _logger.LogTrace("Complete done: {Id}", entry.Id); + } - if (entry.IsAbandoned || entry.IsCompleted) - throw new InvalidOperationException("Queue entry has already been completed or abandoned"); + public override async Task AbandonAsync(IQueueEntry entry) + { + _logger.LogDebug("Queue {Name}:{QueueId} abandon item: {Id}", _options.Name, QueueId, entry.Id); - if (!_dequeued.TryRemove(entry.Id, out var targetEntry) || targetEntry == null) - { - foreach (var kvp in _queue) - { - if (kvp.Id == entry.Id) - throw new Exception("Unable to remove item from the dequeued list (item is in queue)"); - } - foreach (var kvp in _deadletterQueue) - { - if (kvp.Id == entry.Id) - throw new Exception("Unable to remove item from the dequeued list (item is in dead letter)"); - } + if (entry.IsAbandoned || entry.IsCompleted) + throw new InvalidOperationException("Queue entry has already been completed or abandoned"); - throw new Exception("Unable to remove item from the dequeued list"); + if (!_dequeued.TryRemove(entry.Id, out var targetEntry) || targetEntry == null) + { + foreach (var kvp in _queue) + { + if (kvp.Id == entry.Id) + throw new Exception("Unable to remove item from the dequeued list (item is in queue)"); } - - entry.MarkAbandoned(); - Interlocked.Increment(ref _abandonedCount); - _logger.LogTrace("Abandon complete: {Id}", entry.Id); - - try + foreach (var kvp in _deadletterQueue) { - await OnAbandonedAsync(entry).AnyContext(); + if (kvp.Id == entry.Id) + throw new Exception("Unable to remove item from the dequeued list (item is in dead letter)"); } - finally + + throw new Exception("Unable to remove item from the dequeued list"); + } + + entry.MarkAbandoned(); + Interlocked.Increment(ref _abandonedCount); + _logger.LogTrace("Abandon complete: {Id}", entry.Id); + + try + { + await OnAbandonedAsync(entry).AnyContext(); + } + finally + { + if (targetEntry.Attempts < _options.Retries + 1) { - if (targetEntry.Attempts < _options.Retries + 1) + if (_options.RetryDelay > TimeSpan.Zero) { - if (_options.RetryDelay > TimeSpan.Zero) - { - _logger.LogTrace("Adding item to wait list for future retry: {Id}", entry.Id); - var unawaited = Run.DelayedAsync(GetRetryDelay(targetEntry.Attempts), () => RetryAsync(targetEntry), _queueDisposedCancellationTokenSource.Token); - } - else - { - _logger.LogTrace("Adding item back to queue for retry: {Id}", entry.Id); - _ = Task.Run(() => RetryAsync(targetEntry)); - } + _logger.LogTrace("Adding item to wait list for future retry: {Id}", entry.Id); + var unawaited = Run.DelayedAsync(GetRetryDelay(targetEntry.Attempts), () => RetryAsync(targetEntry), _queueDisposedCancellationTokenSource.Token); } else { - _logger.LogTrace("Exceeded retry limit moving to deadletter: {Id}", entry.Id); - _deadletterQueue.Enqueue(targetEntry); + _logger.LogTrace("Adding item back to queue for retry: {Id}", entry.Id); + _ = Task.Run(() => RetryAsync(targetEntry)); } } + else + { + _logger.LogTrace("Exceeded retry limit moving to deadletter: {Id}", entry.Id); + _deadletterQueue.Enqueue(targetEntry); + } } + } - private Task RetryAsync(QueueEntry entry) - { - _logger.LogTrace("Queue {Name} retrying item: {Id} Attempts: {Attempts}", _options.Name, entry.Id, entry.Attempts); + private Task RetryAsync(QueueEntry entry) + { + _logger.LogTrace("Queue {Name} retrying item: {Id} Attempts: {Attempts}", _options.Name, entry.Id, entry.Attempts); - entry.Reset(); - _queue.Enqueue(entry); - _autoResetEvent.Set(); - return Task.CompletedTask; - } + entry.Reset(); + _queue.Enqueue(entry); + _autoResetEvent.Set(); + return Task.CompletedTask; + } - private TimeSpan GetRetryDelay(int attempts) - { - int maxMultiplier = _options.RetryMultipliers.Length > 0 ? _options.RetryMultipliers.Last() : 1; - int multiplier = attempts <= _options.RetryMultipliers.Length ? _options.RetryMultipliers[attempts - 1] : maxMultiplier; - return TimeSpan.FromMilliseconds((int)(_options.RetryDelay.TotalMilliseconds * multiplier)); - } + private TimeSpan GetRetryDelay(int attempts) + { + int maxMultiplier = _options.RetryMultipliers.Length > 0 ? _options.RetryMultipliers.Last() : 1; + int multiplier = attempts <= _options.RetryMultipliers.Length ? _options.RetryMultipliers[attempts - 1] : maxMultiplier; + return TimeSpan.FromMilliseconds((int)(_options.RetryDelay.TotalMilliseconds * multiplier)); + } - protected override Task> GetDeadletterItemsImplAsync(CancellationToken cancellationToken) - { - return Task.FromResult(_deadletterQueue.Select(i => i.Value)); - } + protected override Task> GetDeadletterItemsImplAsync(CancellationToken cancellationToken) + { + return Task.FromResult(_deadletterQueue.Select(i => i.Value)); + } - public override Task DeleteQueueAsync() - { - _logger.LogTrace("Deleting queue: {Name}", _options.Name); - - _queue.Clear(); - _deadletterQueue.Clear(); - _dequeued.Clear(); - _enqueuedCount = 0; - _dequeuedCount = 0; - _completedCount = 0; - _abandonedCount = 0; - _workerErrorCount = 0; - - return Task.CompletedTask; - } + public override Task DeleteQueueAsync() + { + _logger.LogTrace("Deleting queue: {Name}", _options.Name); + + _queue.Clear(); + _deadletterQueue.Clear(); + _dequeued.Clear(); + _enqueuedCount = 0; + _dequeuedCount = 0; + _completedCount = 0; + _abandonedCount = 0; + _workerErrorCount = 0; + + return Task.CompletedTask; + } - protected override async Task DoMaintenanceAsync() - { - var utcNow = SystemClock.UtcNow; - var minAbandonAt = DateTime.MaxValue; + protected override async Task DoMaintenanceAsync() + { + var utcNow = SystemClock.UtcNow; + var minAbandonAt = DateTime.MaxValue; - try + try + { + foreach (var entry in _dequeued.Values.ToList()) { - foreach (var entry in _dequeued.Values.ToList()) + var abandonAt = entry.RenewedTimeUtc.Add(_options.WorkItemTimeout); + if (abandonAt < utcNow) { - var abandonAt = entry.RenewedTimeUtc.Add(_options.WorkItemTimeout); - if (abandonAt < utcNow) - { - _logger.LogInformation("DoMaintenance Abandon: {Id}", entry.Id); + _logger.LogInformation("DoMaintenance Abandon: {Id}", entry.Id); - await AbandonAsync(entry).AnyContext(); - Interlocked.Increment(ref _workerItemTimeoutCount); - } - else if (abandonAt < minAbandonAt) - minAbandonAt = abandonAt; + await AbandonAsync(entry).AnyContext(); + Interlocked.Increment(ref _workerItemTimeoutCount); } + else if (abandonAt < minAbandonAt) + minAbandonAt = abandonAt; } - catch (Exception ex) - { - if (_logger.IsEnabled(LogLevel.Error)) - _logger.LogError(ex, "DoMaintenance Error: {Message}", ex.Message); - } - - return minAbandonAt; } - - public override void Dispose() + catch (Exception ex) { - base.Dispose(); - _queue.Clear(); - _deadletterQueue.Clear(); - _dequeued.Clear(); + if (_logger.IsEnabled(LogLevel.Error)) + _logger.LogError(ex, "DoMaintenance Error: {Message}", ex.Message); + } - _logger.LogTrace("Got {WorkerCount} workers to cleanup", _workers.Count); - foreach (var worker in _workers) - { - if (worker.IsCompleted) - continue; + return minAbandonAt; + } - _logger.LogTrace("Attempting to cleanup worker"); - if (!worker.Wait(TimeSpan.FromSeconds(5))) - _logger.LogError("Failed waiting for worker to stop"); - } + public override void Dispose() + { + base.Dispose(); + _queue.Clear(); + _deadletterQueue.Clear(); + _dequeued.Clear(); + + _logger.LogTrace("Got {WorkerCount} workers to cleanup", _workers.Count); + foreach (var worker in _workers) + { + if (worker.IsCompleted) + continue; + + _logger.LogTrace("Attempting to cleanup worker"); + if (!worker.Wait(TimeSpan.FromSeconds(5))) + _logger.LogError("Failed waiting for worker to stop"); } } } diff --git a/src/Foundatio/Queues/InMemoryQueueOptions.cs b/src/Foundatio/Queues/InMemoryQueueOptions.cs index 1f95d368c..a5ba7fe80 100644 --- a/src/Foundatio/Queues/InMemoryQueueOptions.cs +++ b/src/Foundatio/Queues/InMemoryQueueOptions.cs @@ -1,50 +1,49 @@ using System; -namespace Foundatio.Queues +namespace Foundatio.Queues; + +public class InMemoryQueueOptions : SharedQueueOptions where T : class +{ + public TimeSpan RetryDelay { get; set; } = TimeSpan.FromMinutes(1); + public int CompletedEntryRetentionLimit { get; set; } = 100; + public int[] RetryMultipliers { get; set; } = { 1, 3, 5, 10 }; +} + +public class InMemoryQueueOptionsBuilder : SharedQueueOptionsBuilder, InMemoryQueueOptionsBuilder> where T : class { - public class InMemoryQueueOptions : SharedQueueOptions where T : class + public InMemoryQueueOptionsBuilder RetryDelay(TimeSpan retryDelay) { - public TimeSpan RetryDelay { get; set; } = TimeSpan.FromMinutes(1); - public int CompletedEntryRetentionLimit { get; set; } = 100; - public int[] RetryMultipliers { get; set; } = { 1, 3, 5, 10 }; + if (retryDelay == null) + throw new ArgumentNullException(nameof(retryDelay)); + + if (retryDelay < TimeSpan.Zero) + throw new ArgumentOutOfRangeException(nameof(retryDelay)); + + Target.RetryDelay = retryDelay; + return this; } - public class InMemoryQueueOptionsBuilder : SharedQueueOptionsBuilder, InMemoryQueueOptionsBuilder> where T : class + public InMemoryQueueOptionsBuilder CompletedEntryRetentionLimit(int retentionCount) { - public InMemoryQueueOptionsBuilder RetryDelay(TimeSpan retryDelay) - { - if (retryDelay == null) - throw new ArgumentNullException(nameof(retryDelay)); + if (retentionCount < 0) + throw new ArgumentOutOfRangeException(nameof(retentionCount)); - if (retryDelay < TimeSpan.Zero) - throw new ArgumentOutOfRangeException(nameof(retryDelay)); + Target.CompletedEntryRetentionLimit = retentionCount; + return this; + } - Target.RetryDelay = retryDelay; - return this; - } + public InMemoryQueueOptionsBuilder RetryMultipliers(int[] multipliers) + { + if (multipliers == null) + throw new ArgumentNullException(nameof(multipliers)); - public InMemoryQueueOptionsBuilder CompletedEntryRetentionLimit(int retentionCount) + foreach (int multiplier in multipliers) { - if (retentionCount < 0) - throw new ArgumentOutOfRangeException(nameof(retentionCount)); - - Target.CompletedEntryRetentionLimit = retentionCount; - return this; + if (multiplier < 1) + throw new ArgumentOutOfRangeException(nameof(multipliers)); } - public InMemoryQueueOptionsBuilder RetryMultipliers(int[] multipliers) - { - if (multipliers == null) - throw new ArgumentNullException(nameof(multipliers)); - - foreach (int multiplier in multipliers) - { - if (multiplier < 1) - throw new ArgumentOutOfRangeException(nameof(multipliers)); - } - - Target.RetryMultipliers = multipliers; - return this; - } + Target.RetryMultipliers = multipliers; + return this; } } diff --git a/src/Foundatio/Queues/MetricsQueueBehavior.cs b/src/Foundatio/Queues/MetricsQueueBehavior.cs index e4e14fd44..4fd99e83e 100644 --- a/src/Foundatio/Queues/MetricsQueueBehavior.cs +++ b/src/Foundatio/Queues/MetricsQueueBehavior.cs @@ -5,154 +5,153 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -namespace Foundatio.Queues +namespace Foundatio.Queues; + +[Obsolete("MetricsQueueBehavior is no longer needed. Metrics are built into queues.")] +public class MetricsQueueBehavior : QueueBehaviorBase where T : class { - [Obsolete("MetricsQueueBehavior is no longer needed. Metrics are built into queues.")] - public class MetricsQueueBehavior : QueueBehaviorBase where T : class - { - private readonly string _metricsPrefix; - private readonly IMetricsClient _metricsClient; - private readonly ILogger _logger; - private readonly ScheduledTimer _timer; - private readonly TimeSpan _reportInterval; + private readonly string _metricsPrefix; + private readonly IMetricsClient _metricsClient; + private readonly ILogger _logger; + private readonly ScheduledTimer _timer; + private readonly TimeSpan _reportInterval; - public MetricsQueueBehavior(IMetricsClient metrics, string metricsPrefix = null, TimeSpan? reportCountsInterval = null, ILoggerFactory loggerFactory = null) - { - _logger = loggerFactory?.CreateLogger>() ?? NullLogger>.Instance; - _metricsClient = metrics ?? NullMetricsClient.Instance; + public MetricsQueueBehavior(IMetricsClient metrics, string metricsPrefix = null, TimeSpan? reportCountsInterval = null, ILoggerFactory loggerFactory = null) + { + _logger = loggerFactory?.CreateLogger>() ?? NullLogger>.Instance; + _metricsClient = metrics ?? NullMetricsClient.Instance; - if (!reportCountsInterval.HasValue) - reportCountsInterval = TimeSpan.FromMilliseconds(500); + if (!reportCountsInterval.HasValue) + reportCountsInterval = TimeSpan.FromMilliseconds(500); - _reportInterval = reportCountsInterval.Value > TimeSpan.Zero ? reportCountsInterval.Value : TimeSpan.FromMilliseconds(250); - if (!String.IsNullOrEmpty(metricsPrefix) && !metricsPrefix.EndsWith(".")) - metricsPrefix += "."; + _reportInterval = reportCountsInterval.Value > TimeSpan.Zero ? reportCountsInterval.Value : TimeSpan.FromMilliseconds(250); + if (!String.IsNullOrEmpty(metricsPrefix) && !metricsPrefix.EndsWith(".")) + metricsPrefix += "."; - metricsPrefix += typeof(T).Name.ToLowerInvariant(); - _metricsPrefix = metricsPrefix; - _timer = new ScheduledTimer(ReportQueueCountAsync, loggerFactory: loggerFactory); - } + metricsPrefix += typeof(T).Name.ToLowerInvariant(); + _metricsPrefix = metricsPrefix; + _timer = new ScheduledTimer(ReportQueueCountAsync, loggerFactory: loggerFactory); + } - private async Task ReportQueueCountAsync() + private async Task ReportQueueCountAsync() + { + try { - try - { - var stats = await _queue.GetQueueStatsAsync().AnyContext(); - _logger.LogTrace("Reporting queue count"); - - _metricsClient.Gauge(GetFullMetricName("count"), stats.Queued); - _metricsClient.Gauge(GetFullMetricName("working"), stats.Working); - _metricsClient.Gauge(GetFullMetricName("deadletter"), stats.Deadletter); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error reporting queue metrics"); - } - - return null; - } + var stats = await _queue.GetQueueStatsAsync().AnyContext(); + _logger.LogTrace("Reporting queue count"); - protected override Task OnEnqueued(object sender, EnqueuedEventArgs enqueuedEventArgs) + _metricsClient.Gauge(GetFullMetricName("count"), stats.Queued); + _metricsClient.Gauge(GetFullMetricName("working"), stats.Working); + _metricsClient.Gauge(GetFullMetricName("deadletter"), stats.Deadletter); + } + catch (Exception ex) { - _timer.ScheduleNext(SystemClock.UtcNow.Add(_reportInterval)); - - string subMetricName = GetSubMetricName(enqueuedEventArgs.Entry.Value); - if (!String.IsNullOrEmpty(subMetricName)) - _metricsClient.Counter(GetFullMetricName(subMetricName, "enqueued")); - - _metricsClient.Counter(GetFullMetricName("enqueued")); - return Task.CompletedTask; + _logger.LogError(ex, "Error reporting queue metrics"); } - protected override Task OnDequeued(object sender, DequeuedEventArgs dequeuedEventArgs) - { - _timer.ScheduleNext(SystemClock.UtcNow.Add(_reportInterval)); + return null; + } - var metadata = dequeuedEventArgs.Entry as IQueueEntryMetadata; - string subMetricName = GetSubMetricName(dequeuedEventArgs.Entry.Value); + protected override Task OnEnqueued(object sender, EnqueuedEventArgs enqueuedEventArgs) + { + _timer.ScheduleNext(SystemClock.UtcNow.Add(_reportInterval)); - if (!String.IsNullOrEmpty(subMetricName)) - _metricsClient.Counter(GetFullMetricName(subMetricName, "dequeued")); - _metricsClient.Counter(GetFullMetricName("dequeued")); + string subMetricName = GetSubMetricName(enqueuedEventArgs.Entry.Value); + if (!String.IsNullOrEmpty(subMetricName)) + _metricsClient.Counter(GetFullMetricName(subMetricName, "enqueued")); - if (metadata == null || metadata.EnqueuedTimeUtc == DateTime.MinValue || metadata.DequeuedTimeUtc == DateTime.MinValue) - return Task.CompletedTask; + _metricsClient.Counter(GetFullMetricName("enqueued")); + return Task.CompletedTask; + } - var start = metadata.EnqueuedTimeUtc; - var end = metadata.DequeuedTimeUtc; - int time = (int)(end - start).TotalMilliseconds; + protected override Task OnDequeued(object sender, DequeuedEventArgs dequeuedEventArgs) + { + _timer.ScheduleNext(SystemClock.UtcNow.Add(_reportInterval)); - if (!String.IsNullOrEmpty(subMetricName)) - _metricsClient.Timer(GetFullMetricName(subMetricName, "queuetime"), time); - _metricsClient.Timer(GetFullMetricName("queuetime"), time); + var metadata = dequeuedEventArgs.Entry as IQueueEntryMetadata; + string subMetricName = GetSubMetricName(dequeuedEventArgs.Entry.Value); - return Task.CompletedTask; - } + if (!String.IsNullOrEmpty(subMetricName)) + _metricsClient.Counter(GetFullMetricName(subMetricName, "dequeued")); + _metricsClient.Counter(GetFullMetricName("dequeued")); - protected override Task OnCompleted(object sender, CompletedEventArgs completedEventArgs) - { - _timer.ScheduleNext(SystemClock.UtcNow.Add(_reportInterval)); + if (metadata == null || metadata.EnqueuedTimeUtc == DateTime.MinValue || metadata.DequeuedTimeUtc == DateTime.MinValue) + return Task.CompletedTask; - if (!(completedEventArgs.Entry is IQueueEntryMetadata metadata)) - return Task.CompletedTask; + var start = metadata.EnqueuedTimeUtc; + var end = metadata.DequeuedTimeUtc; + int time = (int)(end - start).TotalMilliseconds; - string subMetricName = GetSubMetricName(completedEventArgs.Entry.Value); - if (!String.IsNullOrEmpty(subMetricName)) - _metricsClient.Counter(GetFullMetricName(subMetricName, "completed")); - _metricsClient.Counter(GetFullMetricName("completed")); + if (!String.IsNullOrEmpty(subMetricName)) + _metricsClient.Timer(GetFullMetricName(subMetricName, "queuetime"), time); + _metricsClient.Timer(GetFullMetricName("queuetime"), time); - int time = (int)metadata.ProcessingTime.TotalMilliseconds; - if (!String.IsNullOrEmpty(subMetricName)) - _metricsClient.Timer(GetFullMetricName(subMetricName, "processtime"), time); - _metricsClient.Timer(GetFullMetricName("processtime"), time); + return Task.CompletedTask; + } - int totalTime = (int)metadata.TotalTime.TotalMilliseconds; - if (!String.IsNullOrEmpty(subMetricName)) - _metricsClient.Timer(GetFullMetricName(subMetricName, "totaltime"), totalTime); - _metricsClient.Timer(GetFullMetricName("totaltime"), totalTime); + protected override Task OnCompleted(object sender, CompletedEventArgs completedEventArgs) + { + _timer.ScheduleNext(SystemClock.UtcNow.Add(_reportInterval)); + if (!(completedEventArgs.Entry is IQueueEntryMetadata metadata)) return Task.CompletedTask; - } - protected override Task OnAbandoned(object sender, AbandonedEventArgs abandonedEventArgs) - { - _timer.ScheduleNext(SystemClock.UtcNow.Add(_reportInterval)); + string subMetricName = GetSubMetricName(completedEventArgs.Entry.Value); + if (!String.IsNullOrEmpty(subMetricName)) + _metricsClient.Counter(GetFullMetricName(subMetricName, "completed")); + _metricsClient.Counter(GetFullMetricName("completed")); + + int time = (int)metadata.ProcessingTime.TotalMilliseconds; + if (!String.IsNullOrEmpty(subMetricName)) + _metricsClient.Timer(GetFullMetricName(subMetricName, "processtime"), time); + _metricsClient.Timer(GetFullMetricName("processtime"), time); + + int totalTime = (int)metadata.TotalTime.TotalMilliseconds; + if (!String.IsNullOrEmpty(subMetricName)) + _metricsClient.Timer(GetFullMetricName(subMetricName, "totaltime"), totalTime); + _metricsClient.Timer(GetFullMetricName("totaltime"), totalTime); - if (!(abandonedEventArgs.Entry is IQueueEntryMetadata metadata)) - return Task.CompletedTask; + return Task.CompletedTask; + } - string subMetricName = GetSubMetricName(abandonedEventArgs.Entry.Value); - if (!String.IsNullOrEmpty(subMetricName)) - _metricsClient.Counter(GetFullMetricName(subMetricName, "abandoned")); - _metricsClient.Counter(GetFullMetricName("abandoned")); + protected override Task OnAbandoned(object sender, AbandonedEventArgs abandonedEventArgs) + { + _timer.ScheduleNext(SystemClock.UtcNow.Add(_reportInterval)); - int time = (int)metadata.ProcessingTime.TotalMilliseconds; - if (!String.IsNullOrEmpty(subMetricName)) - _metricsClient.Timer(GetFullMetricName(subMetricName, "processtime"), time); - _metricsClient.Timer(GetFullMetricName("processtime"), time); + if (!(abandonedEventArgs.Entry is IQueueEntryMetadata metadata)) return Task.CompletedTask; - } - protected string GetSubMetricName(T data) - { - var haveStatName = data as IHaveSubMetricName; - return haveStatName?.SubMetricName; - } + string subMetricName = GetSubMetricName(abandonedEventArgs.Entry.Value); + if (!String.IsNullOrEmpty(subMetricName)) + _metricsClient.Counter(GetFullMetricName(subMetricName, "abandoned")); + _metricsClient.Counter(GetFullMetricName("abandoned")); - protected string GetFullMetricName(string name) - { - return String.Concat(_metricsPrefix, ".", name); - } + int time = (int)metadata.ProcessingTime.TotalMilliseconds; + if (!String.IsNullOrEmpty(subMetricName)) + _metricsClient.Timer(GetFullMetricName(subMetricName, "processtime"), time); + _metricsClient.Timer(GetFullMetricName("processtime"), time); + return Task.CompletedTask; + } - protected string GetFullMetricName(string customMetricName, string name) - { - return String.IsNullOrEmpty(customMetricName) ? GetFullMetricName(name) : String.Concat(_metricsPrefix, ".", customMetricName.ToLower(), ".", name); - } + protected string GetSubMetricName(T data) + { + var haveStatName = data as IHaveSubMetricName; + return haveStatName?.SubMetricName; + } - public override void Dispose() - { - _timer?.Dispose(); - base.Dispose(); - } + protected string GetFullMetricName(string name) + { + return String.Concat(_metricsPrefix, ".", name); + } + + protected string GetFullMetricName(string customMetricName, string name) + { + return String.IsNullOrEmpty(customMetricName) ? GetFullMetricName(name) : String.Concat(_metricsPrefix, ".", customMetricName.ToLower(), ".", name); + } + + public override void Dispose() + { + _timer?.Dispose(); + base.Dispose(); } } diff --git a/src/Foundatio/Queues/QueueBase.cs b/src/Foundatio/Queues/QueueBase.cs index 80e22eaa8..3c948565d 100644 --- a/src/Foundatio/Queues/QueueBase.cs +++ b/src/Foundatio/Queues/QueueBase.cs @@ -11,370 +11,369 @@ using Foundatio.Utility; using Microsoft.Extensions.Logging; -namespace Foundatio.Queues +namespace Foundatio.Queues; + +public abstract class QueueBase : MaintenanceBase, IQueue, IQueueActivity where T : class where TOptions : SharedQueueOptions { - public abstract class QueueBase : MaintenanceBase, IQueue, IQueueActivity where T : class where TOptions : SharedQueueOptions - { - protected readonly TOptions _options; - private readonly string _metricsPrefix; - protected readonly ISerializer _serializer; - - private readonly Counter _enqueuedCounter; - private readonly Counter _dequeuedCounter; - private readonly Histogram _queueTimeHistogram; - private readonly Counter _completedCounter; - private readonly Histogram _processTimeHistogram; - private readonly Histogram _totalTimeHistogram; - private readonly Counter _abandonedCounter; + protected readonly TOptions _options; + private readonly string _metricsPrefix; + protected readonly ISerializer _serializer; + + private readonly Counter _enqueuedCounter; + private readonly Counter _dequeuedCounter; + private readonly Histogram _queueTimeHistogram; + private readonly Counter _completedCounter; + private readonly Histogram _processTimeHistogram; + private readonly Histogram _totalTimeHistogram; + private readonly Counter _abandonedCounter; #pragma warning disable IDE0052 // Remove unread private members - private readonly ObservableGauge _countGauge; - private readonly ObservableGauge _workingGauge; - private readonly ObservableGauge _deadletterGauge; + private readonly ObservableGauge _countGauge; + private readonly ObservableGauge _workingGauge; + private readonly ObservableGauge _deadletterGauge; #pragma warning restore IDE0052 // Remove unread private members - private readonly TagList _emptyTags = default; + private readonly TagList _emptyTags = default; - private readonly List> _behaviors = new(); - protected readonly CancellationTokenSource _queueDisposedCancellationTokenSource; - private bool _isDisposed; + private readonly List> _behaviors = new(); + protected readonly CancellationTokenSource _queueDisposedCancellationTokenSource; + private bool _isDisposed; - protected QueueBase(TOptions options) : base(options?.LoggerFactory) - { - _options = options ?? throw new ArgumentNullException(nameof(options)); - _metricsPrefix = $"foundatio.{typeof(T).Name.ToLowerInvariant()}"; + protected QueueBase(TOptions options) : base(options?.LoggerFactory) + { + _options = options ?? throw new ArgumentNullException(nameof(options)); + _metricsPrefix = $"foundatio.{typeof(T).Name.ToLowerInvariant()}"; - QueueId = options.Name + Guid.NewGuid().ToString("N").Substring(10); + QueueId = options.Name + Guid.NewGuid().ToString("N").Substring(10); - _serializer = options.Serializer ?? DefaultSerializer.Instance; - options.Behaviors.ForEach(AttachBehavior); + _serializer = options.Serializer ?? DefaultSerializer.Instance; + options.Behaviors.ForEach(AttachBehavior); - _queueDisposedCancellationTokenSource = new CancellationTokenSource(); + _queueDisposedCancellationTokenSource = new CancellationTokenSource(); - // setup meters - _enqueuedCounter = FoundatioDiagnostics.Meter.CreateCounter(GetFullMetricName("enqueued"), description: "Number of enqueued items"); - _dequeuedCounter = FoundatioDiagnostics.Meter.CreateCounter(GetFullMetricName("dequeued"), description: "Number of dequeued items"); - _queueTimeHistogram = FoundatioDiagnostics.Meter.CreateHistogram(GetFullMetricName("queuetime"), description: "Time in queue", unit: "ms"); - _completedCounter = FoundatioDiagnostics.Meter.CreateCounter(GetFullMetricName("completed"), description: "Number of completed items"); - _processTimeHistogram = FoundatioDiagnostics.Meter.CreateHistogram(GetFullMetricName("processtime"), description: "Time to process items", unit: "ms"); - _totalTimeHistogram = FoundatioDiagnostics.Meter.CreateHistogram(GetFullMetricName("totaltime"), description: "Total time in queue", unit: "ms"); - _abandonedCounter = FoundatioDiagnostics.Meter.CreateCounter(GetFullMetricName("abandoned"), description: "Number of abandoned items"); + // setup meters + _enqueuedCounter = FoundatioDiagnostics.Meter.CreateCounter(GetFullMetricName("enqueued"), description: "Number of enqueued items"); + _dequeuedCounter = FoundatioDiagnostics.Meter.CreateCounter(GetFullMetricName("dequeued"), description: "Number of dequeued items"); + _queueTimeHistogram = FoundatioDiagnostics.Meter.CreateHistogram(GetFullMetricName("queuetime"), description: "Time in queue", unit: "ms"); + _completedCounter = FoundatioDiagnostics.Meter.CreateCounter(GetFullMetricName("completed"), description: "Number of completed items"); + _processTimeHistogram = FoundatioDiagnostics.Meter.CreateHistogram(GetFullMetricName("processtime"), description: "Time to process items", unit: "ms"); + _totalTimeHistogram = FoundatioDiagnostics.Meter.CreateHistogram(GetFullMetricName("totaltime"), description: "Total time in queue", unit: "ms"); + _abandonedCounter = FoundatioDiagnostics.Meter.CreateCounter(GetFullMetricName("abandoned"), description: "Number of abandoned items"); - var queueMetricValues = new InstrumentsValues(() => + var queueMetricValues = new InstrumentsValues(() => + { + try { - try - { - var stats = GetMetricsQueueStats(); - return (stats.Queued, stats.Working, stats.Deadletter); - } - catch - { - return (0, 0, 0); - } - }); - - _countGauge = FoundatioDiagnostics.Meter.CreateObservableGauge(GetFullMetricName("count"), () => new Measurement(queueMetricValues.GetValue1()), description: "Number of items in the queue"); - _workingGauge = FoundatioDiagnostics.Meter.CreateObservableGauge(GetFullMetricName("working"), () => new Measurement(queueMetricValues.GetValue2()), description: "Number of items currently being processed"); - _deadletterGauge = FoundatioDiagnostics.Meter.CreateObservableGauge(GetFullMetricName("deadletter"), () => new Measurement(queueMetricValues.GetValue3()), description: "Number of items in the deadletter queue"); - } + var stats = GetMetricsQueueStats(); + return (stats.Queued, stats.Working, stats.Deadletter); + } + catch + { + return (0, 0, 0); + } + }); - public string QueueId { get; protected set; } - public DateTime? LastEnqueueActivity { get; protected set; } - public DateTime? LastDequeueActivity { get; protected set; } - ISerializer IHaveSerializer.Serializer => _serializer; + _countGauge = FoundatioDiagnostics.Meter.CreateObservableGauge(GetFullMetricName("count"), () => new Measurement(queueMetricValues.GetValue1()), description: "Number of items in the queue"); + _workingGauge = FoundatioDiagnostics.Meter.CreateObservableGauge(GetFullMetricName("working"), () => new Measurement(queueMetricValues.GetValue2()), description: "Number of items currently being processed"); + _deadletterGauge = FoundatioDiagnostics.Meter.CreateObservableGauge(GetFullMetricName("deadletter"), () => new Measurement(queueMetricValues.GetValue3()), description: "Number of items in the deadletter queue"); + } - public void AttachBehavior(IQueueBehavior behavior) - { - if (behavior == null) - return; + public string QueueId { get; protected set; } + public DateTime? LastEnqueueActivity { get; protected set; } + public DateTime? LastDequeueActivity { get; protected set; } + ISerializer IHaveSerializer.Serializer => _serializer; - _behaviors.Add(behavior); - behavior.Attach(this); - } + public void AttachBehavior(IQueueBehavior behavior) + { + if (behavior == null) + return; - protected abstract Task EnsureQueueCreatedAsync(CancellationToken cancellationToken = default); + _behaviors.Add(behavior); + behavior.Attach(this); + } - protected abstract Task EnqueueImplAsync(T data, QueueEntryOptions options); - public async Task EnqueueAsync(T data, QueueEntryOptions options = null) - { - await EnsureQueueCreatedAsync(_queueDisposedCancellationTokenSource.Token).AnyContext(); + protected abstract Task EnsureQueueCreatedAsync(CancellationToken cancellationToken = default); - LastEnqueueActivity = SystemClock.UtcNow; - options ??= new QueueEntryOptions(); + protected abstract Task EnqueueImplAsync(T data, QueueEntryOptions options); + public async Task EnqueueAsync(T data, QueueEntryOptions options = null) + { + await EnsureQueueCreatedAsync(_queueDisposedCancellationTokenSource.Token).AnyContext(); - return await EnqueueImplAsync(data, options).AnyContext(); - } + LastEnqueueActivity = SystemClock.UtcNow; + options ??= new QueueEntryOptions(); - protected abstract Task> DequeueImplAsync(CancellationToken linkedCancellationToken); - public async Task> DequeueAsync(CancellationToken cancellationToken) - { - await EnsureQueueCreatedAsync(_queueDisposedCancellationTokenSource.Token).AnyContext(); + return await EnqueueImplAsync(data, options).AnyContext(); + } - LastDequeueActivity = SystemClock.UtcNow; - using var linkedCancellationToken = GetLinkedDisposableCancellationTokenSource(cancellationToken); - return await DequeueImplAsync(linkedCancellationToken.Token).AnyContext(); - } + protected abstract Task> DequeueImplAsync(CancellationToken linkedCancellationToken); + public async Task> DequeueAsync(CancellationToken cancellationToken) + { + await EnsureQueueCreatedAsync(_queueDisposedCancellationTokenSource.Token).AnyContext(); - public virtual async Task> DequeueAsync(TimeSpan? timeout = null) - { - using var timeoutCancellationTokenSource = timeout.ToCancellationTokenSource(TimeSpan.FromSeconds(30)); - return await DequeueAsync(timeoutCancellationTokenSource.Token).AnyContext(); - } + LastDequeueActivity = SystemClock.UtcNow; + using var linkedCancellationToken = GetLinkedDisposableCancellationTokenSource(cancellationToken); + return await DequeueImplAsync(linkedCancellationToken.Token).AnyContext(); + } - public abstract Task RenewLockAsync(IQueueEntry queueEntry); + public virtual async Task> DequeueAsync(TimeSpan? timeout = null) + { + using var timeoutCancellationTokenSource = timeout.ToCancellationTokenSource(TimeSpan.FromSeconds(30)); + return await DequeueAsync(timeoutCancellationTokenSource.Token).AnyContext(); + } - public abstract Task CompleteAsync(IQueueEntry queueEntry); + public abstract Task RenewLockAsync(IQueueEntry queueEntry); - public abstract Task AbandonAsync(IQueueEntry queueEntry); + public abstract Task CompleteAsync(IQueueEntry queueEntry); - protected abstract Task> GetDeadletterItemsImplAsync(CancellationToken cancellationToken); - public async Task> GetDeadletterItemsAsync(CancellationToken cancellationToken = default) - { - await EnsureQueueCreatedAsync(_queueDisposedCancellationTokenSource.Token).AnyContext(); - return await GetDeadletterItemsImplAsync(cancellationToken).AnyContext(); - } + public abstract Task AbandonAsync(IQueueEntry queueEntry); + + protected abstract Task> GetDeadletterItemsImplAsync(CancellationToken cancellationToken); + public async Task> GetDeadletterItemsAsync(CancellationToken cancellationToken = default) + { + await EnsureQueueCreatedAsync(_queueDisposedCancellationTokenSource.Token).AnyContext(); + return await GetDeadletterItemsImplAsync(cancellationToken).AnyContext(); + } - protected abstract Task GetQueueStatsImplAsync(); + protected abstract Task GetQueueStatsImplAsync(); - public Task GetQueueStatsAsync() - { - return GetQueueStatsImplAsync(); - } + public Task GetQueueStatsAsync() + { + return GetQueueStatsImplAsync(); + } - protected virtual QueueStats GetMetricsQueueStats() - { - return GetQueueStatsAsync().GetAwaiter().GetResult(); - } + protected virtual QueueStats GetMetricsQueueStats() + { + return GetQueueStatsAsync().GetAwaiter().GetResult(); + } - public abstract Task DeleteQueueAsync(); + public abstract Task DeleteQueueAsync(); - protected abstract void StartWorkingImpl(Func, CancellationToken, Task> handler, bool autoComplete, CancellationToken cancellationToken); - public async Task StartWorkingAsync(Func, CancellationToken, Task> handler, bool autoComplete = false, CancellationToken cancellationToken = default) - { - await EnsureQueueCreatedAsync(_queueDisposedCancellationTokenSource.Token).AnyContext(); - StartWorkingImpl(handler, autoComplete, cancellationToken); - } + protected abstract void StartWorkingImpl(Func, CancellationToken, Task> handler, bool autoComplete, CancellationToken cancellationToken); + public async Task StartWorkingAsync(Func, CancellationToken, Task> handler, bool autoComplete = false, CancellationToken cancellationToken = default) + { + await EnsureQueueCreatedAsync(_queueDisposedCancellationTokenSource.Token).AnyContext(); + StartWorkingImpl(handler, autoComplete, cancellationToken); + } - public IReadOnlyCollection> Behaviors => _behaviors; + public IReadOnlyCollection> Behaviors => _behaviors; - public AsyncEvent> Enqueuing { get; } = new AsyncEvent>(); + public AsyncEvent> Enqueuing { get; } = new AsyncEvent>(); - protected virtual async Task OnEnqueuingAsync(T data, QueueEntryOptions options) + protected virtual async Task OnEnqueuingAsync(T data, QueueEntryOptions options) + { + if (String.IsNullOrEmpty(options.CorrelationId)) { - if (String.IsNullOrEmpty(options.CorrelationId)) - { - options.CorrelationId = Activity.Current?.Id; - if (!String.IsNullOrEmpty(Activity.Current?.TraceStateString)) - options.Properties.Add("TraceState", Activity.Current.TraceStateString); - } + options.CorrelationId = Activity.Current?.Id; + if (!String.IsNullOrEmpty(Activity.Current?.TraceStateString)) + options.Properties.Add("TraceState", Activity.Current.TraceStateString); + } - var enqueueing = Enqueuing; - if (enqueueing == null) - return false; + var enqueueing = Enqueuing; + if (enqueueing == null) + return false; - var args = new EnqueuingEventArgs { Queue = this, Data = data, Options = options }; - await enqueueing.InvokeAsync(this, args).AnyContext(); + var args = new EnqueuingEventArgs { Queue = this, Data = data, Options = options }; + await enqueueing.InvokeAsync(this, args).AnyContext(); - return !args.Cancel; - } + return !args.Cancel; + } - public AsyncEvent> Enqueued { get; } = new AsyncEvent>(true); + public AsyncEvent> Enqueued { get; } = new AsyncEvent>(true); - protected virtual Task OnEnqueuedAsync(IQueueEntry entry) - { - LastEnqueueActivity = SystemClock.UtcNow; + protected virtual Task OnEnqueuedAsync(IQueueEntry entry) + { + LastEnqueueActivity = SystemClock.UtcNow; - var tags = GetQueueEntryTags(entry); - _enqueuedCounter.Add(1, tags); - IncrementSubCounter(entry.Value, "enqueued", tags); + var tags = GetQueueEntryTags(entry); + _enqueuedCounter.Add(1, tags); + IncrementSubCounter(entry.Value, "enqueued", tags); - var enqueued = Enqueued; - if (enqueued == null) - return Task.CompletedTask; + var enqueued = Enqueued; + if (enqueued == null) + return Task.CompletedTask; - var args = new EnqueuedEventArgs { Queue = this, Entry = entry }; - return enqueued.InvokeAsync(this, args); - } + var args = new EnqueuedEventArgs { Queue = this, Entry = entry }; + return enqueued.InvokeAsync(this, args); + } - public AsyncEvent> Dequeued { get; } = new AsyncEvent>(true); + public AsyncEvent> Dequeued { get; } = new AsyncEvent>(true); - protected virtual Task OnDequeuedAsync(IQueueEntry entry) - { - LastDequeueActivity = SystemClock.UtcNow; + protected virtual Task OnDequeuedAsync(IQueueEntry entry) + { + LastDequeueActivity = SystemClock.UtcNow; - var tags = GetQueueEntryTags(entry); - _dequeuedCounter.Add(1, tags); - IncrementSubCounter(entry.Value, "dequeued", tags); + var tags = GetQueueEntryTags(entry); + _dequeuedCounter.Add(1, tags); + IncrementSubCounter(entry.Value, "dequeued", tags); - var metadata = entry as IQueueEntryMetadata; - if (metadata != null && (metadata.EnqueuedTimeUtc != DateTime.MinValue || metadata.DequeuedTimeUtc != DateTime.MinValue)) - { - var start = metadata.EnqueuedTimeUtc; - var end = metadata.DequeuedTimeUtc; - int time = (int)(end - start).TotalMilliseconds; + var metadata = entry as IQueueEntryMetadata; + if (metadata != null && (metadata.EnqueuedTimeUtc != DateTime.MinValue || metadata.DequeuedTimeUtc != DateTime.MinValue)) + { + var start = metadata.EnqueuedTimeUtc; + var end = metadata.DequeuedTimeUtc; + int time = (int)(end - start).TotalMilliseconds; - _queueTimeHistogram.Record(time, tags); - RecordSubHistogram(entry.Value, "queuetime", time, tags); - } + _queueTimeHistogram.Record(time, tags); + RecordSubHistogram(entry.Value, "queuetime", time, tags); + } - var dequeued = Dequeued; - if (dequeued == null) - return Task.CompletedTask; + var dequeued = Dequeued; + if (dequeued == null) + return Task.CompletedTask; - var args = new DequeuedEventArgs { Queue = this, Entry = entry }; - return dequeued.InvokeAsync(this, args); - } + var args = new DequeuedEventArgs { Queue = this, Entry = entry }; + return dequeued.InvokeAsync(this, args); + } - protected virtual TagList GetQueueEntryTags(IQueueEntry entry) - { - return _emptyTags; - } + protected virtual TagList GetQueueEntryTags(IQueueEntry entry) + { + return _emptyTags; + } - public AsyncEvent> LockRenewed { get; } = new AsyncEvent>(true); + public AsyncEvent> LockRenewed { get; } = new AsyncEvent>(true); - protected virtual Task OnLockRenewedAsync(IQueueEntry entry) - { - LastDequeueActivity = SystemClock.UtcNow; + protected virtual Task OnLockRenewedAsync(IQueueEntry entry) + { + LastDequeueActivity = SystemClock.UtcNow; - var lockRenewed = LockRenewed; - if (lockRenewed == null) - return Task.CompletedTask; + var lockRenewed = LockRenewed; + if (lockRenewed == null) + return Task.CompletedTask; - var args = new LockRenewedEventArgs { Queue = this, Entry = entry }; - return lockRenewed.InvokeAsync(this, args); - } + var args = new LockRenewedEventArgs { Queue = this, Entry = entry }; + return lockRenewed.InvokeAsync(this, args); + } - public AsyncEvent> Completed { get; } = new AsyncEvent>(true); + public AsyncEvent> Completed { get; } = new AsyncEvent>(true); - protected virtual async Task OnCompletedAsync(IQueueEntry entry) - { - var now = SystemClock.UtcNow; - LastDequeueActivity = now; + protected virtual async Task OnCompletedAsync(IQueueEntry entry) + { + var now = SystemClock.UtcNow; + LastDequeueActivity = now; - var tags = GetQueueEntryTags(entry); - _completedCounter.Add(1, tags); - IncrementSubCounter(entry.Value, "completed", tags); + var tags = GetQueueEntryTags(entry); + _completedCounter.Add(1, tags); + IncrementSubCounter(entry.Value, "completed", tags); - if (entry is QueueEntry metadata) + if (entry is QueueEntry metadata) + { + if (metadata.EnqueuedTimeUtc > DateTime.MinValue) { - if (metadata.EnqueuedTimeUtc > DateTime.MinValue) - { - metadata.TotalTime = now.Subtract(metadata.EnqueuedTimeUtc); - _totalTimeHistogram.Record((int)metadata.TotalTime.TotalMilliseconds, tags); - RecordSubHistogram(entry.Value, "totaltime", (int)metadata.TotalTime.TotalMilliseconds, tags); - } - - if (metadata.DequeuedTimeUtc > DateTime.MinValue) - { - metadata.ProcessingTime = now.Subtract(metadata.DequeuedTimeUtc); - _processTimeHistogram.Record((int)metadata.ProcessingTime.TotalMilliseconds, tags); - RecordSubHistogram(entry.Value, "processtime", (int)metadata.ProcessingTime.TotalMilliseconds, tags); - } + metadata.TotalTime = now.Subtract(metadata.EnqueuedTimeUtc); + _totalTimeHistogram.Record((int)metadata.TotalTime.TotalMilliseconds, tags); + RecordSubHistogram(entry.Value, "totaltime", (int)metadata.TotalTime.TotalMilliseconds, tags); } - if (Completed != null) + if (metadata.DequeuedTimeUtc > DateTime.MinValue) { - var args = new CompletedEventArgs { Queue = this, Entry = entry }; - await Completed.InvokeAsync(this, args).AnyContext(); + metadata.ProcessingTime = now.Subtract(metadata.DequeuedTimeUtc); + _processTimeHistogram.Record((int)metadata.ProcessingTime.TotalMilliseconds, tags); + RecordSubHistogram(entry.Value, "processtime", (int)metadata.ProcessingTime.TotalMilliseconds, tags); } } - public AsyncEvent> Abandoned { get; } = new AsyncEvent>(true); - - protected virtual async Task OnAbandonedAsync(IQueueEntry entry) + if (Completed != null) { - LastDequeueActivity = SystemClock.UtcNow; + var args = new CompletedEventArgs { Queue = this, Entry = entry }; + await Completed.InvokeAsync(this, args).AnyContext(); + } + } - var tags = GetQueueEntryTags(entry); - _abandonedCounter.Add(1, tags); - IncrementSubCounter(entry.Value, "abandoned", tags); + public AsyncEvent> Abandoned { get; } = new AsyncEvent>(true); - if (entry is QueueEntry metadata && metadata.DequeuedTimeUtc > DateTime.MinValue) - { - metadata.ProcessingTime = SystemClock.UtcNow.Subtract(metadata.DequeuedTimeUtc); - _processTimeHistogram.Record((int)metadata.ProcessingTime.TotalMilliseconds, tags); - RecordSubHistogram(entry.Value, "processtime", (int)metadata.ProcessingTime.TotalMilliseconds, tags); - } + protected virtual async Task OnAbandonedAsync(IQueueEntry entry) + { + LastDequeueActivity = SystemClock.UtcNow; - if (Abandoned != null) - { - var args = new AbandonedEventArgs { Queue = this, Entry = entry }; - await Abandoned.InvokeAsync(this, args).AnyContext(); - } - } + var tags = GetQueueEntryTags(entry); + _abandonedCounter.Add(1, tags); + IncrementSubCounter(entry.Value, "abandoned", tags); - protected string GetSubMetricName(T data) + if (entry is QueueEntry metadata && metadata.DequeuedTimeUtc > DateTime.MinValue) { - var haveStatName = data as IHaveSubMetricName; - return haveStatName?.SubMetricName; + metadata.ProcessingTime = SystemClock.UtcNow.Subtract(metadata.DequeuedTimeUtc); + _processTimeHistogram.Record((int)metadata.ProcessingTime.TotalMilliseconds, tags); + RecordSubHistogram(entry.Value, "processtime", (int)metadata.ProcessingTime.TotalMilliseconds, tags); } - protected readonly ConcurrentDictionary> _counters = new(); - private void IncrementSubCounter(T data, string name, in TagList tags) + if (Abandoned != null) { - if (data is not IHaveSubMetricName) - return; + var args = new AbandonedEventArgs { Queue = this, Entry = entry }; + await Abandoned.InvokeAsync(this, args).AnyContext(); + } + } - string subMetricName = GetSubMetricName(data); - if (String.IsNullOrEmpty(subMetricName)) - return; + protected string GetSubMetricName(T data) + { + var haveStatName = data as IHaveSubMetricName; + return haveStatName?.SubMetricName; + } - var fullName = GetFullMetricName(subMetricName, name); - _counters.GetOrAdd(fullName, FoundatioDiagnostics.Meter.CreateCounter(fullName)).Add(1, tags); - } + protected readonly ConcurrentDictionary> _counters = new(); + private void IncrementSubCounter(T data, string name, in TagList tags) + { + if (data is not IHaveSubMetricName) + return; - protected readonly ConcurrentDictionary> _histograms = new(); - private void RecordSubHistogram(T data, string name, int value, in TagList tags) - { - if (data is not IHaveSubMetricName) - return; + string subMetricName = GetSubMetricName(data); + if (String.IsNullOrEmpty(subMetricName)) + return; - string subMetricName = GetSubMetricName(data); - if (String.IsNullOrEmpty(subMetricName)) - return; + var fullName = GetFullMetricName(subMetricName, name); + _counters.GetOrAdd(fullName, FoundatioDiagnostics.Meter.CreateCounter(fullName)).Add(1, tags); + } - var fullName = GetFullMetricName(subMetricName, name); - _histograms.GetOrAdd(fullName, FoundatioDiagnostics.Meter.CreateHistogram(fullName)).Record(value, tags); - } + protected readonly ConcurrentDictionary> _histograms = new(); + private void RecordSubHistogram(T data, string name, int value, in TagList tags) + { + if (data is not IHaveSubMetricName) + return; - protected string GetFullMetricName(string name) - { - return String.Concat(_metricsPrefix, ".", name); - } + string subMetricName = GetSubMetricName(data); + if (String.IsNullOrEmpty(subMetricName)) + return; - protected string GetFullMetricName(string customMetricName, string name) - { - return String.IsNullOrEmpty(customMetricName) ? GetFullMetricName(name) : String.Concat(_metricsPrefix, ".", customMetricName.ToLower(), ".", name); - } + var fullName = GetFullMetricName(subMetricName, name); + _histograms.GetOrAdd(fullName, FoundatioDiagnostics.Meter.CreateHistogram(fullName)).Record(value, tags); + } - protected CancellationTokenSource GetLinkedDisposableCancellationTokenSource(CancellationToken cancellationToken) - { - return CancellationTokenSource.CreateLinkedTokenSource(_queueDisposedCancellationTokenSource.Token, cancellationToken); - } + protected string GetFullMetricName(string name) + { + return String.Concat(_metricsPrefix, ".", name); + } + + protected string GetFullMetricName(string customMetricName, string name) + { + return String.IsNullOrEmpty(customMetricName) ? GetFullMetricName(name) : String.Concat(_metricsPrefix, ".", customMetricName.ToLower(), ".", name); + } + + protected CancellationTokenSource GetLinkedDisposableCancellationTokenSource(CancellationToken cancellationToken) + { + return CancellationTokenSource.CreateLinkedTokenSource(_queueDisposedCancellationTokenSource.Token, cancellationToken); + } - public override void Dispose() + public override void Dispose() + { + if (_isDisposed) { - if (_isDisposed) - { - _logger.LogTrace("Queue {Name} ({Id}) dispose was already called.", _options.Name, QueueId); - return; - } + _logger.LogTrace("Queue {Name} ({Id}) dispose was already called.", _options.Name, QueueId); + return; + } - _isDisposed = true; - _logger.LogTrace("Queue {Name} ({Id}) dispose", _options.Name, QueueId); - _queueDisposedCancellationTokenSource?.Cancel(); - _queueDisposedCancellationTokenSource?.Dispose(); - base.Dispose(); + _isDisposed = true; + _logger.LogTrace("Queue {Name} ({Id}) dispose", _options.Name, QueueId); + _queueDisposedCancellationTokenSource?.Cancel(); + _queueDisposedCancellationTokenSource?.Dispose(); + base.Dispose(); - Abandoned?.Dispose(); - Completed?.Dispose(); - Dequeued?.Dispose(); - Enqueued?.Dispose(); - Enqueuing?.Dispose(); - LockRenewed?.Dispose(); + Abandoned?.Dispose(); + Completed?.Dispose(); + Dequeued?.Dispose(); + Enqueued?.Dispose(); + Enqueuing?.Dispose(); + LockRenewed?.Dispose(); - foreach (var behavior in _behaviors.OfType()) - behavior.Dispose(); + foreach (var behavior in _behaviors.OfType()) + behavior.Dispose(); - _behaviors.Clear(); - } + _behaviors.Clear(); } } diff --git a/src/Foundatio/Queues/QueueBehaviour.cs b/src/Foundatio/Queues/QueueBehaviour.cs index 8c3bcee2f..237bf7db8 100644 --- a/src/Foundatio/Queues/QueueBehaviour.cs +++ b/src/Foundatio/Queues/QueueBehaviour.cs @@ -2,64 +2,63 @@ using System.Collections.Generic; using System.Threading.Tasks; -namespace Foundatio.Queues +namespace Foundatio.Queues; + +public interface IQueueBehavior where T : class { - public interface IQueueBehavior where T : class - { - void Attach(IQueue queue); - } + void Attach(IQueue queue); +} - public abstract class QueueBehaviorBase : IQueueBehavior, IDisposable where T : class - { - protected IQueue _queue; - private readonly List _disposables = new(); +public abstract class QueueBehaviorBase : IQueueBehavior, IDisposable where T : class +{ + protected IQueue _queue; + private readonly List _disposables = new(); - public virtual void Attach(IQueue queue) - { - _queue = queue; + public virtual void Attach(IQueue queue) + { + _queue = queue; - _disposables.Add(_queue.Enqueuing.AddHandler(OnEnqueuing)); - _disposables.Add(_queue.Enqueued.AddHandler(OnEnqueued)); - _disposables.Add(_queue.Dequeued.AddHandler(OnDequeued)); - _disposables.Add(_queue.LockRenewed.AddHandler(OnLockRenewed)); - _disposables.Add(_queue.Completed.AddHandler(OnCompleted)); - _disposables.Add(_queue.Abandoned.AddHandler(OnAbandoned)); - } + _disposables.Add(_queue.Enqueuing.AddHandler(OnEnqueuing)); + _disposables.Add(_queue.Enqueued.AddHandler(OnEnqueued)); + _disposables.Add(_queue.Dequeued.AddHandler(OnDequeued)); + _disposables.Add(_queue.LockRenewed.AddHandler(OnLockRenewed)); + _disposables.Add(_queue.Completed.AddHandler(OnCompleted)); + _disposables.Add(_queue.Abandoned.AddHandler(OnAbandoned)); + } - protected virtual Task OnEnqueuing(object sender, EnqueuingEventArgs enqueuingEventArgs) - { - return Task.CompletedTask; - } + protected virtual Task OnEnqueuing(object sender, EnqueuingEventArgs enqueuingEventArgs) + { + return Task.CompletedTask; + } - protected virtual Task OnEnqueued(object sender, EnqueuedEventArgs enqueuedEventArgs) - { - return Task.CompletedTask; - } + protected virtual Task OnEnqueued(object sender, EnqueuedEventArgs enqueuedEventArgs) + { + return Task.CompletedTask; + } - protected virtual Task OnDequeued(object sender, DequeuedEventArgs dequeuedEventArgs) - { - return Task.CompletedTask; - } + protected virtual Task OnDequeued(object sender, DequeuedEventArgs dequeuedEventArgs) + { + return Task.CompletedTask; + } - protected virtual Task OnLockRenewed(object sender, LockRenewedEventArgs dequeuedEventArgs) - { - return Task.CompletedTask; - } + protected virtual Task OnLockRenewed(object sender, LockRenewedEventArgs dequeuedEventArgs) + { + return Task.CompletedTask; + } - protected virtual Task OnCompleted(object sender, CompletedEventArgs completedEventArgs) - { - return Task.CompletedTask; - } + protected virtual Task OnCompleted(object sender, CompletedEventArgs completedEventArgs) + { + return Task.CompletedTask; + } - protected virtual Task OnAbandoned(object sender, AbandonedEventArgs abandonedEventArgs) - { - return Task.CompletedTask; - } + protected virtual Task OnAbandoned(object sender, AbandonedEventArgs abandonedEventArgs) + { + return Task.CompletedTask; + } - public virtual void Dispose() - { - foreach (var disposable in _disposables) - disposable.Dispose(); - } + public virtual void Dispose() + { + foreach (var disposable in _disposables) + disposable.Dispose(); } } diff --git a/src/Foundatio/Queues/QueueEntry.cs b/src/Foundatio/Queues/QueueEntry.cs index ad4efe673..455810559 100644 --- a/src/Foundatio/Queues/QueueEntry.cs +++ b/src/Foundatio/Queues/QueueEntry.cs @@ -3,90 +3,89 @@ using System.Threading.Tasks; using Foundatio.Utility; -namespace Foundatio.Queues -{ - public class QueueEntry : IQueueEntry, IQueueEntryMetadata, IAsyncDisposable where T : class - { - private readonly IQueue _queue; - private readonly T _original; +namespace Foundatio.Queues; - public QueueEntry(string id, string correlationId, T value, IQueue queue, DateTime enqueuedTimeUtc, int attempts) - { - Id = id; - CorrelationId = correlationId; - _original = value; - Value = value.DeepClone(); - _queue = queue; - EnqueuedTimeUtc = enqueuedTimeUtc; - Attempts = attempts; - DequeuedTimeUtc = RenewedTimeUtc = SystemClock.UtcNow; - } +public class QueueEntry : IQueueEntry, IQueueEntryMetadata, IAsyncDisposable where T : class +{ + private readonly IQueue _queue; + private readonly T _original; - public string Id { get; } - public string CorrelationId { get; } - public IDictionary Properties { get; } = new Dictionary(); - public bool IsCompleted { get; private set; } - public bool IsAbandoned { get; private set; } - public Type EntryType => Value.GetType(); - public object GetValue() => Value; - public T Value { get; set; } - public DateTime EnqueuedTimeUtc { get; set; } - public DateTime RenewedTimeUtc { get; set; } - public DateTime DequeuedTimeUtc { get; set; } - public int Attempts { get; set; } - public TimeSpan ProcessingTime { get; set; } - public TimeSpan TotalTime { get; set; } + public QueueEntry(string id, string correlationId, T value, IQueue queue, DateTime enqueuedTimeUtc, int attempts) + { + Id = id; + CorrelationId = correlationId; + _original = value; + Value = value.DeepClone(); + _queue = queue; + EnqueuedTimeUtc = enqueuedTimeUtc; + Attempts = attempts; + DequeuedTimeUtc = RenewedTimeUtc = SystemClock.UtcNow; + } - void IQueueEntry.MarkCompleted() - { - IsCompleted = true; - } + public string Id { get; } + public string CorrelationId { get; } + public IDictionary Properties { get; } = new Dictionary(); + public bool IsCompleted { get; private set; } + public bool IsAbandoned { get; private set; } + public Type EntryType => Value.GetType(); + public object GetValue() => Value; + public T Value { get; set; } + public DateTime EnqueuedTimeUtc { get; set; } + public DateTime RenewedTimeUtc { get; set; } + public DateTime DequeuedTimeUtc { get; set; } + public int Attempts { get; set; } + public TimeSpan ProcessingTime { get; set; } + public TimeSpan TotalTime { get; set; } - void IQueueEntry.MarkAbandoned() - { - IsAbandoned = true; - } + void IQueueEntry.MarkCompleted() + { + IsCompleted = true; + } - public Task RenewLockAsync() - { - RenewedTimeUtc = SystemClock.UtcNow; - return _queue.RenewLockAsync(this); - } + void IQueueEntry.MarkAbandoned() + { + IsAbandoned = true; + } - public Task CompleteAsync() - { - return _queue.CompleteAsync(this); - } + public Task RenewLockAsync() + { + RenewedTimeUtc = SystemClock.UtcNow; + return _queue.RenewLockAsync(this); + } - public Task AbandonAsync() - { - return _queue.AbandonAsync(this); - } + public Task CompleteAsync() + { + return _queue.CompleteAsync(this); + } - public async ValueTask DisposeAsync() - { - if (!IsAbandoned && !IsCompleted) - await AbandonAsync(); - } + public Task AbandonAsync() + { + return _queue.AbandonAsync(this); + } - internal void Reset() - { - IsCompleted = false; - IsAbandoned = false; - Value = _original.DeepClone(); - } + public async ValueTask DisposeAsync() + { + if (!IsAbandoned && !IsCompleted) + await AbandonAsync(); } - public interface IQueueEntryMetadata + internal void Reset() { - string Id { get; } - string CorrelationId { get; } - IDictionary Properties { get; } - DateTime EnqueuedTimeUtc { get; } - DateTime RenewedTimeUtc { get; } - DateTime DequeuedTimeUtc { get; } - int Attempts { get; } - TimeSpan ProcessingTime { get; } - TimeSpan TotalTime { get; } + IsCompleted = false; + IsAbandoned = false; + Value = _original.DeepClone(); } } + +public interface IQueueEntryMetadata +{ + string Id { get; } + string CorrelationId { get; } + IDictionary Properties { get; } + DateTime EnqueuedTimeUtc { get; } + DateTime RenewedTimeUtc { get; } + DateTime DequeuedTimeUtc { get; } + int Attempts { get; } + TimeSpan ProcessingTime { get; } + TimeSpan TotalTime { get; } +} diff --git a/src/Foundatio/Queues/QueueStatSummary.cs b/src/Foundatio/Queues/QueueStatSummary.cs index 7b8ba7fdb..509b957f4 100644 --- a/src/Foundatio/Queues/QueueStatSummary.cs +++ b/src/Foundatio/Queues/QueueStatSummary.cs @@ -1,17 +1,16 @@ using Foundatio.Metrics; -namespace Foundatio.Queues +namespace Foundatio.Queues; + +public class QueueStatSummary { - public class QueueStatSummary - { - public GaugeStatSummary Count { get; set; } - public GaugeStatSummary Working { get; set; } - public GaugeStatSummary Deadletter { get; set; } - public CounterStatSummary Enqueued { get; set; } - public TimingStatSummary QueueTime { get; set; } - public CounterStatSummary Dequeued { get; set; } - public CounterStatSummary Completed { get; set; } - public CounterStatSummary Abandoned { get; set; } - public TimingStatSummary ProcessTime { get; set; } - } + public GaugeStatSummary Count { get; set; } + public GaugeStatSummary Working { get; set; } + public GaugeStatSummary Deadletter { get; set; } + public CounterStatSummary Enqueued { get; set; } + public TimingStatSummary QueueTime { get; set; } + public CounterStatSummary Dequeued { get; set; } + public CounterStatSummary Completed { get; set; } + public CounterStatSummary Abandoned { get; set; } + public TimingStatSummary ProcessTime { get; set; } } diff --git a/src/Foundatio/Queues/SharedQueueOptions.cs b/src/Foundatio/Queues/SharedQueueOptions.cs index 4d4543524..944e7589a 100644 --- a/src/Foundatio/Queues/SharedQueueOptions.cs +++ b/src/Foundatio/Queues/SharedQueueOptions.cs @@ -1,65 +1,64 @@ using System; using System.Collections.Generic; -namespace Foundatio.Queues +namespace Foundatio.Queues; + +public class SharedQueueOptions : SharedOptions where T : class +{ + public string Name { get; set; } = typeof(T).Name; + public int Retries { get; set; } = 2; + public TimeSpan WorkItemTimeout { get; set; } = TimeSpan.FromMinutes(5); + public ICollection> Behaviors { get; set; } = new List>(); +} + +public class SharedQueueOptionsBuilder : SharedOptionsBuilder + where T : class + where TOptions : SharedQueueOptions, new() + where TBuilder : SharedQueueOptionsBuilder { - public class SharedQueueOptions : SharedOptions where T : class + public TBuilder Name(string name) { - public string Name { get; set; } = typeof(T).Name; - public int Retries { get; set; } = 2; - public TimeSpan WorkItemTimeout { get; set; } = TimeSpan.FromMinutes(5); - public ICollection> Behaviors { get; set; } = new List>(); + if (!String.IsNullOrEmpty(name)) + Target.Name = name; + return (TBuilder)this; } - public class SharedQueueOptionsBuilder : SharedOptionsBuilder - where T : class - where TOptions : SharedQueueOptions, new() - where TBuilder : SharedQueueOptionsBuilder + public TBuilder Retries(int retries) { - public TBuilder Name(string name) - { - if (!String.IsNullOrEmpty(name)) - Target.Name = name; - return (TBuilder)this; - } - - public TBuilder Retries(int retries) - { - if (retries < 0) - throw new ArgumentOutOfRangeException(nameof(retries)); + if (retries < 0) + throw new ArgumentOutOfRangeException(nameof(retries)); - Target.Retries = retries; - return (TBuilder)this; - } + Target.Retries = retries; + return (TBuilder)this; + } - public TBuilder WorkItemTimeout(TimeSpan timeout) - { - if (timeout == null) - throw new ArgumentNullException(nameof(timeout)); + public TBuilder WorkItemTimeout(TimeSpan timeout) + { + if (timeout == null) + throw new ArgumentNullException(nameof(timeout)); - if (timeout < TimeSpan.Zero) - throw new ArgumentOutOfRangeException(nameof(timeout)); + if (timeout < TimeSpan.Zero) + throw new ArgumentOutOfRangeException(nameof(timeout)); - Target.WorkItemTimeout = timeout; - return (TBuilder)this; - } + Target.WorkItemTimeout = timeout; + return (TBuilder)this; + } - public TBuilder Behaviors(params IQueueBehavior[] behaviors) - { - Target.Behaviors = behaviors; - return (TBuilder)this; - } + public TBuilder Behaviors(params IQueueBehavior[] behaviors) + { + Target.Behaviors = behaviors; + return (TBuilder)this; + } - public TBuilder AddBehavior(IQueueBehavior behavior) - { - if (behavior == null) - throw new ArgumentNullException(nameof(behavior)); + public TBuilder AddBehavior(IQueueBehavior behavior) + { + if (behavior == null) + throw new ArgumentNullException(nameof(behavior)); - if (Target.Behaviors == null) - Target.Behaviors = new List>(); - Target.Behaviors.Add(behavior); + if (Target.Behaviors == null) + Target.Behaviors = new List>(); + Target.Behaviors.Add(behavior); - return (TBuilder)this; - } + return (TBuilder)this; } } diff --git a/src/Foundatio/Serializer/IHaveSerializer.cs b/src/Foundatio/Serializer/IHaveSerializer.cs index c452ad418..5359d0e82 100644 --- a/src/Foundatio/Serializer/IHaveSerializer.cs +++ b/src/Foundatio/Serializer/IHaveSerializer.cs @@ -1,7 +1,6 @@ -namespace Foundatio.Serializer +namespace Foundatio.Serializer; + +public interface IHaveSerializer { - public interface IHaveSerializer - { - ISerializer Serializer { get; } - } + ISerializer Serializer { get; } } diff --git a/src/Foundatio/Serializer/ISerializer.cs b/src/Foundatio/Serializer/ISerializer.cs index 1378c9a6d..2e72e8407 100644 --- a/src/Foundatio/Serializer/ISerializer.cs +++ b/src/Foundatio/Serializer/ISerializer.cs @@ -2,85 +2,84 @@ using System.IO; using System.Text; -namespace Foundatio.Serializer +namespace Foundatio.Serializer; + +public interface ISerializer +{ + object Deserialize(Stream data, Type objectType); + void Serialize(object value, Stream output); +} + +public interface ITextSerializer : ISerializer { } + +public static class DefaultSerializer +{ + public static ISerializer Instance { get; set; } = new SystemTextJsonSerializer(); +} + +public static class SerializerExtensions { - public interface ISerializer + public static T Deserialize(this ISerializer serializer, Stream data) { - object Deserialize(Stream data, Type objectType); - void Serialize(object value, Stream output); + return (T)serializer.Deserialize(data, typeof(T)); } - public interface ITextSerializer : ISerializer { } + public static T Deserialize(this ISerializer serializer, byte[] data) + { + return (T)serializer.Deserialize(new MemoryStream(data), typeof(T)); + } - public static class DefaultSerializer + public static object Deserialize(this ISerializer serializer, byte[] data, Type objectType) { - public static ISerializer Instance { get; set; } = new SystemTextJsonSerializer(); + return serializer.Deserialize(new MemoryStream(data), objectType); } - public static class SerializerExtensions + public static T Deserialize(this ISerializer serializer, string data) { - public static T Deserialize(this ISerializer serializer, Stream data) - { - return (T)serializer.Deserialize(data, typeof(T)); - } - - public static T Deserialize(this ISerializer serializer, byte[] data) - { - return (T)serializer.Deserialize(new MemoryStream(data), typeof(T)); - } - - public static object Deserialize(this ISerializer serializer, byte[] data, Type objectType) - { - return serializer.Deserialize(new MemoryStream(data), objectType); - } - - public static T Deserialize(this ISerializer serializer, string data) - { - byte[] bytes; - if (data == null) - bytes = Array.Empty(); - else if (serializer is ITextSerializer) - bytes = Encoding.UTF8.GetBytes(data); - else - bytes = Convert.FromBase64String(data); - - return (T)serializer.Deserialize(new MemoryStream(bytes), typeof(T)); - } - - public static object Deserialize(this ISerializer serializer, string data, Type objectType) - { - byte[] bytes; - if (data == null) - bytes = Array.Empty(); - else if (serializer is ITextSerializer) - bytes = Encoding.UTF8.GetBytes(data); - else - bytes = Convert.FromBase64String(data); - - return serializer.Deserialize(new MemoryStream(bytes), objectType); - } - - public static string SerializeToString(this ISerializer serializer, T value) - { - if (value == null) - return null; - - var bytes = serializer.SerializeToBytes(value); - if (serializer is ITextSerializer) - return Encoding.UTF8.GetString(bytes); - - return Convert.ToBase64String(bytes); - } - - public static byte[] SerializeToBytes(this ISerializer serializer, T value) - { - if (value == null) - return null; - - var stream = new MemoryStream(); - serializer.Serialize(value, stream); - - return stream.ToArray(); - } + byte[] bytes; + if (data == null) + bytes = Array.Empty(); + else if (serializer is ITextSerializer) + bytes = Encoding.UTF8.GetBytes(data); + else + bytes = Convert.FromBase64String(data); + + return (T)serializer.Deserialize(new MemoryStream(bytes), typeof(T)); + } + + public static object Deserialize(this ISerializer serializer, string data, Type objectType) + { + byte[] bytes; + if (data == null) + bytes = Array.Empty(); + else if (serializer is ITextSerializer) + bytes = Encoding.UTF8.GetBytes(data); + else + bytes = Convert.FromBase64String(data); + + return serializer.Deserialize(new MemoryStream(bytes), objectType); + } + + public static string SerializeToString(this ISerializer serializer, T value) + { + if (value == null) + return null; + + var bytes = serializer.SerializeToBytes(value); + if (serializer is ITextSerializer) + return Encoding.UTF8.GetString(bytes); + + return Convert.ToBase64String(bytes); + } + + public static byte[] SerializeToBytes(this ISerializer serializer, T value) + { + if (value == null) + return null; + + var stream = new MemoryStream(); + serializer.Serialize(value, stream); + + return stream.ToArray(); } } diff --git a/src/Foundatio/Serializer/SystemTextJsonSerializer.cs b/src/Foundatio/Serializer/SystemTextJsonSerializer.cs index ea9a6a259..a70239a28 100644 --- a/src/Foundatio/Serializer/SystemTextJsonSerializer.cs +++ b/src/Foundatio/Serializer/SystemTextJsonSerializer.cs @@ -3,73 +3,72 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace Foundatio.Serializer +namespace Foundatio.Serializer; + +public class SystemTextJsonSerializer : ITextSerializer { - public class SystemTextJsonSerializer : ITextSerializer - { - private readonly JsonSerializerOptions _serializeOptions; - private readonly JsonSerializerOptions _deserializeOptions; + private readonly JsonSerializerOptions _serializeOptions; + private readonly JsonSerializerOptions _deserializeOptions; - public SystemTextJsonSerializer(JsonSerializerOptions serializeOptions = null, JsonSerializerOptions deserializeOptions = null) + public SystemTextJsonSerializer(JsonSerializerOptions serializeOptions = null, JsonSerializerOptions deserializeOptions = null) + { + if (serializeOptions != null) { - if (serializeOptions != null) - { - _serializeOptions = serializeOptions; - } - else - { - _serializeOptions = new JsonSerializerOptions(); - } - - if (deserializeOptions != null) - { - _deserializeOptions = deserializeOptions; - } - else - { - _deserializeOptions = new JsonSerializerOptions(); - _deserializeOptions.Converters.Add(new ObjectToInferredTypesConverter()); - } + _serializeOptions = serializeOptions; } - - public void Serialize(object data, Stream outputStream) + else { - var writer = new Utf8JsonWriter(outputStream); - JsonSerializer.Serialize(writer, data, data.GetType(), _serializeOptions); - writer.Flush(); + _serializeOptions = new JsonSerializerOptions(); } - public object Deserialize(Stream inputStream, Type objectType) + if (deserializeOptions != null) { - using var reader = new StreamReader(inputStream); - return JsonSerializer.Deserialize(reader.ReadToEnd(), objectType, _deserializeOptions); + _deserializeOptions = deserializeOptions; + } + else + { + _deserializeOptions = new JsonSerializerOptions(); + _deserializeOptions.Converters.Add(new ObjectToInferredTypesConverter()); } } - public class ObjectToInferredTypesConverter : JsonConverter + public void Serialize(object data, Stream outputStream) { - public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType == JsonTokenType.True) - return true; + var writer = new Utf8JsonWriter(outputStream); + JsonSerializer.Serialize(writer, data, data.GetType(), _serializeOptions); + writer.Flush(); + } - if (reader.TokenType == JsonTokenType.False) - return false; + public object Deserialize(Stream inputStream, Type objectType) + { + using var reader = new StreamReader(inputStream); + return JsonSerializer.Deserialize(reader.ReadToEnd(), objectType, _deserializeOptions); + } +} - if (reader.TokenType == JsonTokenType.Number) - return reader.TryGetInt64(out long number) ? number : (object)reader.GetDouble(); +public class ObjectToInferredTypesConverter : JsonConverter +{ + public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.True) + return true; - if (reader.TokenType == JsonTokenType.String) - return reader.TryGetDateTime(out var datetime) ? datetime : (object)reader.GetString(); + if (reader.TokenType == JsonTokenType.False) + return false; - using var document = JsonDocument.ParseValue(ref reader); + if (reader.TokenType == JsonTokenType.Number) + return reader.TryGetInt64(out long number) ? number : (object)reader.GetDouble(); - return document.RootElement.Clone(); - } + if (reader.TokenType == JsonTokenType.String) + return reader.TryGetDateTime(out var datetime) ? datetime : (object)reader.GetString(); - public override void Write(Utf8JsonWriter writer, object objectToWrite, JsonSerializerOptions options) - { - throw new InvalidOperationException(); - } + using var document = JsonDocument.ParseValue(ref reader); + + return document.RootElement.Clone(); + } + + public override void Write(Utf8JsonWriter writer, object objectToWrite, JsonSerializerOptions options) + { + throw new InvalidOperationException(); } } diff --git a/src/Foundatio/Storage/ActionableStream.cs b/src/Foundatio/Storage/ActionableStream.cs index 615b48656..582804f70 100644 --- a/src/Foundatio/Storage/ActionableStream.cs +++ b/src/Foundatio/Storage/ActionableStream.cs @@ -3,88 +3,87 @@ using System.Threading; using System.Threading.Tasks; -namespace Foundatio.Storage +namespace Foundatio.Storage; + +public class ActionableStream : Stream { - public class ActionableStream : Stream - { - private readonly Action _disposeAction; - private readonly Stream _stream; + private readonly Action _disposeAction; + private readonly Stream _stream; - protected override void Dispose(bool disposing) + protected override void Dispose(bool disposing) + { + try { - try - { - _disposeAction.Invoke(); - } - catch { /* ignore if these are already disposed; this is to make sure they are */ } - - _stream.Dispose(); - base.Dispose(disposing); + _disposeAction.Invoke(); } + catch { /* ignore if these are already disposed; this is to make sure they are */ } - public ActionableStream(Stream stream, Action disposeAction) - { - _stream = stream ?? throw new ArgumentNullException(); - _disposeAction = disposeAction; - } + _stream.Dispose(); + base.Dispose(disposing); + } - public override bool CanRead => _stream.CanRead; + public ActionableStream(Stream stream, Action disposeAction) + { + _stream = stream ?? throw new ArgumentNullException(); + _disposeAction = disposeAction; + } - public override bool CanSeek => _stream.CanSeek; + public override bool CanRead => _stream.CanRead; - public override bool CanWrite => _stream.CanWrite; + public override bool CanSeek => _stream.CanSeek; - public override long Length => _stream.Length; + public override bool CanWrite => _stream.CanWrite; - public override long Position - { - get => _stream.Position; - set => _stream.Position = value; - } + public override long Length => _stream.Length; - public override long Seek(long offset, SeekOrigin origin) - { - return _stream.Seek(offset, origin); - } + public override long Position + { + get => _stream.Position; + set => _stream.Position = value; + } - public override void SetLength(long value) - { - _stream.SetLength(value); - } + public override long Seek(long offset, SeekOrigin origin) + { + return _stream.Seek(offset, origin); + } - public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) - { - return _stream.CopyToAsync(destination, bufferSize, cancellationToken); - } + public override void SetLength(long value) + { + _stream.SetLength(value); + } - public override void Flush() - { - _stream.Flush(); - } + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + return _stream.CopyToAsync(destination, bufferSize, cancellationToken); + } - public override Task FlushAsync(CancellationToken cancellationToken) - { - return _stream.FlushAsync(cancellationToken); - } + public override void Flush() + { + _stream.Flush(); + } - public override int Read(byte[] buffer, int offset, int count) - { - return _stream.Read(buffer, offset, count); - } + public override Task FlushAsync(CancellationToken cancellationToken) + { + return _stream.FlushAsync(cancellationToken); + } - public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - return _stream.ReadAsync(buffer, offset, count, cancellationToken); - } + public override int Read(byte[] buffer, int offset, int count) + { + return _stream.Read(buffer, offset, count); + } - public override void Write(byte[] buffer, int offset, int count) - { - _stream.Write(buffer, offset, count); - } + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _stream.ReadAsync(buffer, offset, count, cancellationToken); + } - public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - return _stream.WriteAsync(buffer, offset, count, cancellationToken); - } + public override void Write(byte[] buffer, int offset, int count) + { + _stream.Write(buffer, offset, count); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _stream.WriteAsync(buffer, offset, count, cancellationToken); } } diff --git a/src/Foundatio/Storage/FolderFileStorage.cs b/src/Foundatio/Storage/FolderFileStorage.cs index ab5ed8348..929f3b90d 100644 --- a/src/Foundatio/Storage/FolderFileStorage.cs +++ b/src/Foundatio/Storage/FolderFileStorage.cs @@ -11,390 +11,389 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -namespace Foundatio.Storage -{ - public class FolderFileStorage : IFileStorage - { - private readonly AsyncLock _lock = new(); - private readonly ISerializer _serializer; - protected readonly ILogger _logger; +namespace Foundatio.Storage; - public FolderFileStorage(FolderFileStorageOptions options) - { - if (options == null) - throw new ArgumentNullException(nameof(options)); +public class FolderFileStorage : IFileStorage +{ + private readonly AsyncLock _lock = new(); + private readonly ISerializer _serializer; + protected readonly ILogger _logger; - _serializer = options.Serializer ?? DefaultSerializer.Instance; - _logger = options.LoggerFactory?.CreateLogger(GetType()) ?? NullLogger.Instance; + public FolderFileStorage(FolderFileStorageOptions options) + { + if (options == null) + throw new ArgumentNullException(nameof(options)); - string folder = PathHelper.ExpandPath(options.Folder); - if (!Path.IsPathRooted(folder)) - folder = Path.GetFullPath(folder); + _serializer = options.Serializer ?? DefaultSerializer.Instance; + _logger = options.LoggerFactory?.CreateLogger(GetType()) ?? NullLogger.Instance; - char lastCharacter = folder[folder.Length - 1]; - if (!lastCharacter.Equals(Path.DirectorySeparatorChar) && !lastCharacter.Equals(Path.AltDirectorySeparatorChar)) - folder += Path.DirectorySeparatorChar; + string folder = PathHelper.ExpandPath(options.Folder); + if (!Path.IsPathRooted(folder)) + folder = Path.GetFullPath(folder); - Folder = folder; + char lastCharacter = folder[folder.Length - 1]; + if (!lastCharacter.Equals(Path.DirectorySeparatorChar) && !lastCharacter.Equals(Path.AltDirectorySeparatorChar)) + folder += Path.DirectorySeparatorChar; - _logger.LogInformation("Creating {Directory} directory", folder); - Directory.CreateDirectory(folder); - } + Folder = folder; - public FolderFileStorage(Builder config) - : this(config(new FolderFileStorageOptionsBuilder()).Build()) { } + _logger.LogInformation("Creating {Directory} directory", folder); + Directory.CreateDirectory(folder); + } - public string Folder { get; set; } - ISerializer IHaveSerializer.Serializer => _serializer; + public FolderFileStorage(Builder config) + : this(config(new FolderFileStorageOptionsBuilder()).Build()) { } - [Obsolete($"Use {nameof(GetFileStreamAsync)} with {nameof(FileAccess)} instead to define read or write behaviour of stream")] - public Task GetFileStreamAsync(string path, CancellationToken cancellationToken = default) - => GetFileStreamAsync(path, StreamMode.Read, cancellationToken); + public string Folder { get; set; } + ISerializer IHaveSerializer.Serializer => _serializer; - public Task GetFileStreamAsync(string path, StreamMode streamMode, CancellationToken cancellationToken = default) - { - var stream = streamMode switch - { - StreamMode.Read => GetFileStreamAsync(path, FileAccess.Read), - StreamMode.Write => GetFileStreamAsync(path, FileAccess.Write), - _ => throw new NotSupportedException($"Stream mode {streamMode} is not supported."), - }; + [Obsolete($"Use {nameof(GetFileStreamAsync)} with {nameof(FileAccess)} instead to define read or write behaviour of stream")] + public Task GetFileStreamAsync(string path, CancellationToken cancellationToken = default) + => GetFileStreamAsync(path, StreamMode.Read, cancellationToken); - return Task.FromResult(stream); - } - - public Stream GetFileStreamAsync(string path, FileAccess fileAccess) + public Task GetFileStreamAsync(string path, StreamMode streamMode, CancellationToken cancellationToken = default) + { + var stream = streamMode switch { - if (String.IsNullOrEmpty(path)) - throw new ArgumentNullException(nameof(path)); + StreamMode.Read => GetFileStreamAsync(path, FileAccess.Read), + StreamMode.Write => GetFileStreamAsync(path, FileAccess.Write), + _ => throw new NotSupportedException($"Stream mode {streamMode} is not supported."), + }; - string normalizedPath = path.NormalizePath(); - string fullPath = Path.Combine(Folder, normalizedPath); - if (fileAccess != FileAccess.Read) - { - CreateFileStream(fullPath).Dispose(); - } + return Task.FromResult(stream); + } - var fileMode = GetFileModeForFileAccess(fileAccess); + public Stream GetFileStreamAsync(string path, FileAccess fileAccess) + { + if (String.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); - try - { - return File.Open(fullPath, fileMode, fileAccess); - } - catch (IOException ex) when (ex is FileNotFoundException or DirectoryNotFoundException) - { - _logger.LogError(ex, "Unable to get file stream for {Path}: {Message}", normalizedPath, ex.Message); - return null; - } + string normalizedPath = path.NormalizePath(); + string fullPath = Path.Combine(Folder, normalizedPath); + if (fileAccess != FileAccess.Read) + { + CreateFileStream(fullPath).Dispose(); } + var fileMode = GetFileModeForFileAccess(fileAccess); - private FileMode GetFileModeForFileAccess(FileAccess fileAccess) + try { - return fileAccess switch - { - FileAccess.Read => FileMode.Open, - FileAccess.Write => FileMode.Create, - FileAccess.ReadWrite => FileMode.OpenOrCreate, - _ => throw new ArgumentOutOfRangeException(nameof(fileAccess), fileAccess, null) - }; + return File.Open(fullPath, fileMode, fileAccess); + } + catch (IOException ex) when (ex is FileNotFoundException or DirectoryNotFoundException) + { + _logger.LogError(ex, "Unable to get file stream for {Path}: {Message}", normalizedPath, ex.Message); + return null; } + } + - public Task GetFileInfoAsync(string path) + private FileMode GetFileModeForFileAccess(FileAccess fileAccess) + { + return fileAccess switch { - if (String.IsNullOrEmpty(path)) - throw new ArgumentNullException(nameof(path)); + FileAccess.Read => FileMode.Open, + FileAccess.Write => FileMode.Create, + FileAccess.ReadWrite => FileMode.OpenOrCreate, + _ => throw new ArgumentOutOfRangeException(nameof(fileAccess), fileAccess, null) + }; + } - string normalizedPath = path.NormalizePath(); - _logger.LogTrace("Getting file stream for {Path}", normalizedPath); + public Task GetFileInfoAsync(string path) + { + if (String.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); - var info = new FileInfo(Path.Combine(Folder, normalizedPath)); - if (!info.Exists) - { - _logger.LogError("Unable to get file info for {Path}: File Not Found", normalizedPath); - return Task.FromResult(null); - } + string normalizedPath = path.NormalizePath(); + _logger.LogTrace("Getting file stream for {Path}", normalizedPath); - return Task.FromResult(new FileSpec - { - Path = normalizedPath.Replace(Folder, String.Empty), - Created = info.CreationTimeUtc, - Modified = info.LastWriteTimeUtc, - Size = info.Length - }); + var info = new FileInfo(Path.Combine(Folder, normalizedPath)); + if (!info.Exists) + { + _logger.LogError("Unable to get file info for {Path}: File Not Found", normalizedPath); + return Task.FromResult(null); } - public Task ExistsAsync(string path) + return Task.FromResult(new FileSpec { - if (String.IsNullOrEmpty(path)) - throw new ArgumentNullException(nameof(path)); + Path = normalizedPath.Replace(Folder, String.Empty), + Created = info.CreationTimeUtc, + Modified = info.LastWriteTimeUtc, + Size = info.Length + }); + } - string normalizedPath = path.NormalizePath(); - _logger.LogTrace("Checking if {Path} exists", normalizedPath); - return Task.FromResult(File.Exists(Path.Combine(Folder, normalizedPath))); - } + public Task ExistsAsync(string path) + { + if (String.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); - public async Task SaveFileAsync(string path, Stream stream, CancellationToken cancellationToken = default) - { - if (String.IsNullOrEmpty(path)) - throw new ArgumentNullException(nameof(path)); - if (stream == null) - throw new ArgumentNullException(nameof(stream)); + string normalizedPath = path.NormalizePath(); + _logger.LogTrace("Checking if {Path} exists", normalizedPath); + return Task.FromResult(File.Exists(Path.Combine(Folder, normalizedPath))); + } - string normalizedPath = path.NormalizePath(); - _logger.LogTrace("Saving {Path}", normalizedPath); - string file = Path.Combine(Folder, normalizedPath); + public async Task SaveFileAsync(string path, Stream stream, CancellationToken cancellationToken = default) + { + if (String.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); + if (stream == null) + throw new ArgumentNullException(nameof(stream)); - try - { - using var fileStream = CreateFileStream(file); - await stream.CopyToAsync(fileStream).AnyContext(); - return true; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error saving {Path}: {Message}", normalizedPath, ex.Message); - return false; - } - } + string normalizedPath = path.NormalizePath(); + _logger.LogTrace("Saving {Path}", normalizedPath); + string file = Path.Combine(Folder, normalizedPath); - private Stream CreateFileStream(string filePath) + try { - try - { - return File.Create(filePath); - } - catch (DirectoryNotFoundException) { } - - string directory = Path.GetDirectoryName(filePath); - if (directory != null) - { - _logger.LogInformation("Creating {Directory} directory", directory); - Directory.CreateDirectory(directory); - } + using var fileStream = CreateFileStream(file); + await stream.CopyToAsync(fileStream).AnyContext(); + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error saving {Path}: {Message}", normalizedPath, ex.Message); + return false; + } + } + private Stream CreateFileStream(string filePath) + { + try + { return File.Create(filePath); } + catch (DirectoryNotFoundException) { } - public async Task RenameFileAsync(string path, string newPath, CancellationToken cancellationToken = default) + string directory = Path.GetDirectoryName(filePath); + if (directory != null) { - if (String.IsNullOrEmpty(path)) - throw new ArgumentNullException(nameof(path)); - if (String.IsNullOrEmpty(newPath)) - throw new ArgumentNullException(nameof(newPath)); + _logger.LogInformation("Creating {Directory} directory", directory); + Directory.CreateDirectory(directory); + } + + return File.Create(filePath); + } + + public async Task RenameFileAsync(string path, string newPath, CancellationToken cancellationToken = default) + { + if (String.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); + if (String.IsNullOrEmpty(newPath)) + throw new ArgumentNullException(nameof(newPath)); - string normalizedPath = path.NormalizePath(); - string normalizedNewPath = newPath.NormalizePath(); - _logger.LogInformation("Renaming {Path} to {NewPath}", normalizedPath, normalizedNewPath); + string normalizedPath = path.NormalizePath(); + string normalizedNewPath = newPath.NormalizePath(); + _logger.LogInformation("Renaming {Path} to {NewPath}", normalizedPath, normalizedNewPath); - try + try + { + using (await _lock.LockAsync().AnyContext()) { - using (await _lock.LockAsync().AnyContext()) + string directory = Path.GetDirectoryName(normalizedNewPath); + if (directory != null) { - string directory = Path.GetDirectoryName(normalizedNewPath); - if (directory != null) - { - _logger.LogInformation("Creating {Directory} directory", directory); - Directory.CreateDirectory(Path.Combine(Folder, directory)); - } - - string oldFullPath = Path.Combine(Folder, normalizedPath); - string newFullPath = Path.Combine(Folder, normalizedNewPath); - try - { - File.Move(oldFullPath, newFullPath); - } - catch (IOException ex) - { - _logger.LogDebug(ex, "Error renaming {Path} to {NewPath}: Deleting {NewFullPath}", normalizedPath, normalizedNewPath, newFullPath); - File.Delete(newFullPath); - - _logger.LogTrace("Renaming {Path} to {NewPath}", normalizedPath, normalizedNewPath); - File.Move(oldFullPath, newFullPath); - } + _logger.LogInformation("Creating {Directory} directory", directory); + Directory.CreateDirectory(Path.Combine(Folder, directory)); } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error renaming {Path} to {NewPath}", normalizedPath, normalizedNewPath); - return false; - } - return true; - } + string oldFullPath = Path.Combine(Folder, normalizedPath); + string newFullPath = Path.Combine(Folder, normalizedNewPath); + try + { + File.Move(oldFullPath, newFullPath); + } + catch (IOException ex) + { + _logger.LogDebug(ex, "Error renaming {Path} to {NewPath}: Deleting {NewFullPath}", normalizedPath, normalizedNewPath, newFullPath); + File.Delete(newFullPath); - public async Task CopyFileAsync(string path, string targetPath, CancellationToken cancellationToken = default) + _logger.LogTrace("Renaming {Path} to {NewPath}", normalizedPath, normalizedNewPath); + File.Move(oldFullPath, newFullPath); + } + } + } + catch (Exception ex) { - if (String.IsNullOrEmpty(path)) - throw new ArgumentNullException(nameof(path)); - if (String.IsNullOrEmpty(targetPath)) - throw new ArgumentNullException(nameof(targetPath)); + _logger.LogError(ex, "Error renaming {Path} to {NewPath}", normalizedPath, normalizedNewPath); + return false; + } - string normalizedPath = path.NormalizePath(); - string normalizedTargetPath = targetPath.NormalizePath(); - _logger.LogInformation("Copying {Path} to {TargetPath}", normalizedPath, normalizedTargetPath); + return true; + } + + public async Task CopyFileAsync(string path, string targetPath, CancellationToken cancellationToken = default) + { + if (String.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); + if (String.IsNullOrEmpty(targetPath)) + throw new ArgumentNullException(nameof(targetPath)); - try + string normalizedPath = path.NormalizePath(); + string normalizedTargetPath = targetPath.NormalizePath(); + _logger.LogInformation("Copying {Path} to {TargetPath}", normalizedPath, normalizedTargetPath); + + try + { + using (await _lock.LockAsync().AnyContext()) { - using (await _lock.LockAsync().AnyContext()) + string directory = Path.GetDirectoryName(normalizedTargetPath); + if (directory != null) { - string directory = Path.GetDirectoryName(normalizedTargetPath); - if (directory != null) - { - _logger.LogInformation("Creating {Directory} directory", directory); - Directory.CreateDirectory(Path.Combine(Folder, directory)); - } - - File.Copy(Path.Combine(Folder, normalizedPath), Path.Combine(Folder, normalizedTargetPath)); + _logger.LogInformation("Creating {Directory} directory", directory); + Directory.CreateDirectory(Path.Combine(Folder, directory)); } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error copying {Path} to {TargetPath}: {Message}", normalizedPath, normalizedTargetPath, ex.Message); - return false; - } - return true; + File.Copy(Path.Combine(Folder, normalizedPath), Path.Combine(Folder, normalizedTargetPath)); + } } - - public Task DeleteFileAsync(string path, CancellationToken cancellationToken = default) + catch (Exception ex) { - if (String.IsNullOrEmpty(path)) - throw new ArgumentNullException(nameof(path)); + _logger.LogError(ex, "Error copying {Path} to {TargetPath}: {Message}", normalizedPath, normalizedTargetPath, ex.Message); + return false; + } - string normalizedPath = path.NormalizePath(); - _logger.LogTrace("Deleting {Path}", normalizedPath); + return true; + } - try - { - File.Delete(Path.Combine(Folder, normalizedPath)); - } - catch (Exception ex) when (ex is FileNotFoundException or DirectoryNotFoundException) - { - _logger.LogError(ex, "Unable to delete {Path}: {Message}", normalizedPath, ex.Message); - return Task.FromResult(false); - } + public Task DeleteFileAsync(string path, CancellationToken cancellationToken = default) + { + if (String.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); - return Task.FromResult(true); - } + string normalizedPath = path.NormalizePath(); + _logger.LogTrace("Deleting {Path}", normalizedPath); - public Task DeleteFilesAsync(string searchPattern = null, CancellationToken cancellation = default) + try { - int count = 0; + File.Delete(Path.Combine(Folder, normalizedPath)); + } + catch (Exception ex) when (ex is FileNotFoundException or DirectoryNotFoundException) + { + _logger.LogError(ex, "Unable to delete {Path}: {Message}", normalizedPath, ex.Message); + return Task.FromResult(false); + } - if (String.IsNullOrEmpty(searchPattern) || searchPattern == "*") - { - if (Directory.Exists(Folder)) - { - _logger.LogInformation("Deleting {Directory} directory", Folder); - count += Directory.EnumerateFiles(Folder, "*,*", SearchOption.AllDirectories).Count(); - Directory.Delete(Folder, true); - _logger.LogTrace("Finished deleting {Directory} directory with {FileCount} files", Folder, count); - } + return Task.FromResult(true); + } - return Task.FromResult(count); - } + public Task DeleteFilesAsync(string searchPattern = null, CancellationToken cancellation = default) + { + int count = 0; - searchPattern = searchPattern.NormalizePath(); - string path = Path.Combine(Folder, searchPattern); - if (path[path.Length - 1] == Path.DirectorySeparatorChar || path.EndsWith(Path.DirectorySeparatorChar + "*")) + if (String.IsNullOrEmpty(searchPattern) || searchPattern == "*") + { + if (Directory.Exists(Folder)) { - string directory = Path.GetDirectoryName(path); - if (Directory.Exists(directory)) - { - _logger.LogInformation("Deleting {Directory} directory", directory); - count += Directory.EnumerateFiles(directory, "*,*", SearchOption.AllDirectories).Count(); - Directory.Delete(directory, true); - _logger.LogTrace("Finished deleting {Directory} directory with {FileCount} files", directory, count); - return Task.FromResult(count); - } - - return Task.FromResult(0); + _logger.LogInformation("Deleting {Directory} directory", Folder); + count += Directory.EnumerateFiles(Folder, "*,*", SearchOption.AllDirectories).Count(); + Directory.Delete(Folder, true); + _logger.LogTrace("Finished deleting {Directory} directory with {FileCount} files", Folder, count); } - if (Directory.Exists(path)) + return Task.FromResult(count); + } + + searchPattern = searchPattern.NormalizePath(); + string path = Path.Combine(Folder, searchPattern); + if (path[path.Length - 1] == Path.DirectorySeparatorChar || path.EndsWith(Path.DirectorySeparatorChar + "*")) + { + string directory = Path.GetDirectoryName(path); + if (Directory.Exists(directory)) { - _logger.LogInformation("Deleting {Directory} directory", path); - count += Directory.EnumerateFiles(path, "*,*", SearchOption.AllDirectories).Count(); - Directory.Delete(path, true); - _logger.LogTrace("Finished deleting {Directory} directory with {FileCount} files", path, count); + _logger.LogInformation("Deleting {Directory} directory", directory); + count += Directory.EnumerateFiles(directory, "*,*", SearchOption.AllDirectories).Count(); + Directory.Delete(directory, true); + _logger.LogTrace("Finished deleting {Directory} directory with {FileCount} files", directory, count); return Task.FromResult(count); } - _logger.LogInformation("Deleting files matching {SearchPattern}", searchPattern); - foreach (string file in Directory.EnumerateFiles(Folder, searchPattern, SearchOption.AllDirectories)) - { - _logger.LogTrace("Deleting {Path}", file); - File.Delete(file); - count++; - } + return Task.FromResult(0); + } - _logger.LogTrace("Finished deleting {FileCount} files matching {SearchPattern}", count, searchPattern); + if (Directory.Exists(path)) + { + _logger.LogInformation("Deleting {Directory} directory", path); + count += Directory.EnumerateFiles(path, "*,*", SearchOption.AllDirectories).Count(); + Directory.Delete(path, true); + _logger.LogTrace("Finished deleting {Directory} directory with {FileCount} files", path, count); return Task.FromResult(count); - } - public async Task GetPagedFileListAsync(int pageSize = 100, string searchPattern = null, CancellationToken cancellationToken = default) + _logger.LogInformation("Deleting files matching {SearchPattern}", searchPattern); + foreach (string file in Directory.EnumerateFiles(Folder, searchPattern, SearchOption.AllDirectories)) { - if (pageSize <= 0) - return PagedFileListResult.Empty; + _logger.LogTrace("Deleting {Path}", file); + File.Delete(file); + count++; + } - if (String.IsNullOrEmpty(searchPattern)) - searchPattern = "*"; + _logger.LogTrace("Finished deleting {FileCount} files matching {SearchPattern}", count, searchPattern); + return Task.FromResult(count); - searchPattern = searchPattern.NormalizePath(); + } - if (!Directory.Exists(Path.GetDirectoryName(Path.Combine(Folder, searchPattern)))) - { - _logger.LogTrace("Returning empty file list matching {SearchPattern}: Directory Not Found", searchPattern); - return PagedFileListResult.Empty; - } + public async Task GetPagedFileListAsync(int pageSize = 100, string searchPattern = null, CancellationToken cancellationToken = default) + { + if (pageSize <= 0) + return PagedFileListResult.Empty; - var result = new PagedFileListResult(s => Task.FromResult(GetFiles(searchPattern, 1, pageSize))); - await result.NextPageAsync().AnyContext(); - return result; - } + if (String.IsNullOrEmpty(searchPattern)) + searchPattern = "*"; - private NextPageResult GetFiles(string searchPattern, int page, int pageSize) + searchPattern = searchPattern.NormalizePath(); + + if (!Directory.Exists(Path.GetDirectoryName(Path.Combine(Folder, searchPattern)))) { - var list = new List(); - int pagingLimit = pageSize; - int skip = (page - 1) * pagingLimit; - if (pagingLimit < Int32.MaxValue) - pagingLimit++; - - _logger.LogTrace(s => s.Property("Limit", pagingLimit).Property("Skip", skip), "Getting file list matching {SearchPattern}...", searchPattern); - foreach (string path in Directory.EnumerateFiles(Folder, searchPattern, SearchOption.AllDirectories).Skip(skip).Take(pagingLimit)) - { - var info = new FileInfo(path); - if (!info.Exists) - continue; + _logger.LogTrace("Returning empty file list matching {SearchPattern}: Directory Not Found", searchPattern); + return PagedFileListResult.Empty; + } - list.Add(new FileSpec - { - Path = info.FullName.Replace(Folder, String.Empty), - Created = info.CreationTimeUtc, - Modified = info.LastWriteTimeUtc, - Size = info.Length - }); - } + var result = new PagedFileListResult(s => Task.FromResult(GetFiles(searchPattern, 1, pageSize))); + await result.NextPageAsync().AnyContext(); + return result; + } - bool hasMore = false; - if (list.Count == pagingLimit) - { - hasMore = true; - list.RemoveAt(pagingLimit - 1); - } + private NextPageResult GetFiles(string searchPattern, int page, int pageSize) + { + var list = new List(); + int pagingLimit = pageSize; + int skip = (page - 1) * pagingLimit; + if (pagingLimit < Int32.MaxValue) + pagingLimit++; + + _logger.LogTrace(s => s.Property("Limit", pagingLimit).Property("Skip", skip), "Getting file list matching {SearchPattern}...", searchPattern); + foreach (string path in Directory.EnumerateFiles(Folder, searchPattern, SearchOption.AllDirectories).Skip(skip).Take(pagingLimit)) + { + var info = new FileInfo(path); + if (!info.Exists) + continue; - return new NextPageResult + list.Add(new FileSpec { - Success = true, - HasMore = hasMore, - Files = list, - NextPageFunc = hasMore ? _ => Task.FromResult(GetFiles(searchPattern, page + 1, pageSize)) : null - }; + Path = info.FullName.Replace(Folder, String.Empty), + Created = info.CreationTimeUtc, + Modified = info.LastWriteTimeUtc, + Size = info.Length + }); } - public void Dispose() { } + bool hasMore = false; + if (list.Count == pagingLimit) + { + hasMore = true; + list.RemoveAt(pagingLimit - 1); + } + + return new NextPageResult + { + Success = true, + HasMore = hasMore, + Files = list, + NextPageFunc = hasMore ? _ => Task.FromResult(GetFiles(searchPattern, page + 1, pageSize)) : null + }; } + + public void Dispose() { } } diff --git a/src/Foundatio/Storage/FolderFileStorageOptions.cs b/src/Foundatio/Storage/FolderFileStorageOptions.cs index 5335bb6cc..9de979e48 100644 --- a/src/Foundatio/Storage/FolderFileStorageOptions.cs +++ b/src/Foundatio/Storage/FolderFileStorageOptions.cs @@ -1,20 +1,19 @@ using System; -namespace Foundatio.Storage +namespace Foundatio.Storage; + +public class FolderFileStorageOptions : SharedOptions { - public class FolderFileStorageOptions : SharedOptions - { - public string Folder { get; set; } - } + public string Folder { get; set; } +} - public class FolderFileStorageOptionsBuilder : SharedOptionsBuilder +public class FolderFileStorageOptionsBuilder : SharedOptionsBuilder +{ + public FolderFileStorageOptionsBuilder Folder(string folder) { - public FolderFileStorageOptionsBuilder Folder(string folder) - { - if (string.IsNullOrEmpty(folder)) - throw new ArgumentNullException(nameof(folder)); - Target.Folder = folder; - return this; - } + if (string.IsNullOrEmpty(folder)) + throw new ArgumentNullException(nameof(folder)); + Target.Folder = folder; + return this; } } diff --git a/src/Foundatio/Storage/IFileStorage.cs b/src/Foundatio/Storage/IFileStorage.cs index dadfa9fee..a5295c355 100644 --- a/src/Foundatio/Storage/IFileStorage.cs +++ b/src/Foundatio/Storage/IFileStorage.cs @@ -9,192 +9,191 @@ using Foundatio.Serializer; using Foundatio.Utility; -namespace Foundatio.Storage +namespace Foundatio.Storage; + +public interface IFileStorage : IHaveSerializer, IDisposable +{ + [Obsolete($"Use {nameof(GetFileStreamAsync)} with {nameof(FileAccess)} instead to define read or write behaviour of stream")] + Task GetFileStreamAsync(string path, CancellationToken cancellationToken = default); + /// + /// Gets a file stream in the specified mode + /// + /// Path to the file in the file storage + /// What the stream is used for + /// Token to cancel + /// Stream in the specified mode + Task GetFileStreamAsync(string path, StreamMode streamMode, CancellationToken cancellationToken = default); + Task GetFileInfoAsync(string path); + Task ExistsAsync(string path); + Task SaveFileAsync(string path, Stream stream, CancellationToken cancellationToken = default); + Task RenameFileAsync(string path, string newPath, CancellationToken cancellationToken = default); + Task CopyFileAsync(string path, string targetPath, CancellationToken cancellationToken = default); + Task DeleteFileAsync(string path, CancellationToken cancellationToken = default); + Task DeleteFilesAsync(string searchPattern = null, CancellationToken cancellation = default); + Task GetPagedFileListAsync(int pageSize = 100, string searchPattern = null, CancellationToken cancellationToken = default); +} + +public interface IHasNextPageFunc +{ + Func> NextPageFunc { get; set; } +} + +public class NextPageResult { - public interface IFileStorage : IHaveSerializer, IDisposable + public bool Success { get; set; } + public bool HasMore { get; set; } + public IReadOnlyCollection Files { get; set; } + public Func> NextPageFunc { get; set; } +} + +public class PagedFileListResult : IHasNextPageFunc +{ + private static readonly IReadOnlyCollection _empty = new ReadOnlyCollection(Array.Empty()); + public static readonly PagedFileListResult Empty = new(_empty); + + public PagedFileListResult(IReadOnlyCollection files) { - [Obsolete($"Use {nameof(GetFileStreamAsync)} with {nameof(FileAccess)} instead to define read or write behaviour of stream")] - Task GetFileStreamAsync(string path, CancellationToken cancellationToken = default); - /// - /// Gets a file stream in the specified mode - /// - /// Path to the file in the file storage - /// What the stream is used for - /// Token to cancel - /// Stream in the specified mode - Task GetFileStreamAsync(string path, StreamMode streamMode, CancellationToken cancellationToken = default); - Task GetFileInfoAsync(string path); - Task ExistsAsync(string path); - Task SaveFileAsync(string path, Stream stream, CancellationToken cancellationToken = default); - Task RenameFileAsync(string path, string newPath, CancellationToken cancellationToken = default); - Task CopyFileAsync(string path, string targetPath, CancellationToken cancellationToken = default); - Task DeleteFileAsync(string path, CancellationToken cancellationToken = default); - Task DeleteFilesAsync(string searchPattern = null, CancellationToken cancellation = default); - Task GetPagedFileListAsync(int pageSize = 100, string searchPattern = null, CancellationToken cancellationToken = default); + Files = files; + HasMore = false; + ((IHasNextPageFunc)this).NextPageFunc = null; } - public interface IHasNextPageFunc + public PagedFileListResult(IReadOnlyCollection files, bool hasMore, Func> nextPageFunc) { - Func> NextPageFunc { get; set; } + Files = files; + HasMore = hasMore; + ((IHasNextPageFunc)this).NextPageFunc = nextPageFunc; } - public class NextPageResult + public PagedFileListResult(Func> nextPageFunc) { - public bool Success { get; set; } - public bool HasMore { get; set; } - public IReadOnlyCollection Files { get; set; } - public Func> NextPageFunc { get; set; } + ((IHasNextPageFunc)this).NextPageFunc = nextPageFunc; } - public class PagedFileListResult : IHasNextPageFunc - { - private static readonly IReadOnlyCollection _empty = new ReadOnlyCollection(Array.Empty()); - public static readonly PagedFileListResult Empty = new(_empty); + public IReadOnlyCollection Files { get; private set; } + public bool HasMore { get; private set; } + protected IDictionary Data { get; } = new DataDictionary(); + Func> IHasNextPageFunc.NextPageFunc { get; set; } - public PagedFileListResult(IReadOnlyCollection files) - { - Files = files; - HasMore = false; - ((IHasNextPageFunc)this).NextPageFunc = null; - } + public async Task NextPageAsync() + { + if (((IHasNextPageFunc)this).NextPageFunc == null) + return false; - public PagedFileListResult(IReadOnlyCollection files, bool hasMore, Func> nextPageFunc) + var result = await ((IHasNextPageFunc)this).NextPageFunc(this).AnyContext(); + if (result.Success) { - Files = files; - HasMore = hasMore; - ((IHasNextPageFunc)this).NextPageFunc = nextPageFunc; + Files = result.Files; + HasMore = result.HasMore; + ((IHasNextPageFunc)this).NextPageFunc = result.NextPageFunc; } - - public PagedFileListResult(Func> nextPageFunc) + else { - ((IHasNextPageFunc)this).NextPageFunc = nextPageFunc; + Files = _empty; + HasMore = false; + ((IHasNextPageFunc)this).NextPageFunc = null; } - public IReadOnlyCollection Files { get; private set; } - public bool HasMore { get; private set; } - protected IDictionary Data { get; } = new DataDictionary(); - Func> IHasNextPageFunc.NextPageFunc { get; set; } - - public async Task NextPageAsync() - { - if (((IHasNextPageFunc)this).NextPageFunc == null) - return false; - - var result = await ((IHasNextPageFunc)this).NextPageFunc(this).AnyContext(); - if (result.Success) - { - Files = result.Files; - HasMore = result.HasMore; - ((IHasNextPageFunc)this).NextPageFunc = result.NextPageFunc; - } - else - { - Files = _empty; - HasMore = false; - ((IHasNextPageFunc)this).NextPageFunc = null; - } - - return result.Success; - } + return result.Success; } +} + +[DebuggerDisplay("Path = {Path}, Created = {Created}, Modified = {Modified}, Size = {Size} bytes")] +public class FileSpec +{ + public string Path { get; set; } + public DateTime Created { get; set; } + public DateTime Modified { get; set; } + + /// + /// In Bytes + /// + public long Size { get; set; } + // TODO: Add metadata object for custom properties +} - [DebuggerDisplay("Path = {Path}, Created = {Created}, Modified = {Modified}, Size = {Size} bytes")] - public class FileSpec +public static class FileStorageExtensions +{ + public static Task SaveObjectAsync(this IFileStorage storage, string path, T data, CancellationToken cancellationToken = default) { - public string Path { get; set; } - public DateTime Created { get; set; } - public DateTime Modified { get; set; } - - /// - /// In Bytes - /// - public long Size { get; set; } - // TODO: Add metadata object for custom properties + if (String.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); + + var bytes = storage.Serializer.SerializeToBytes(data); + return storage.SaveFileAsync(path, new MemoryStream(bytes), cancellationToken); } - public static class FileStorageExtensions + public static async Task GetObjectAsync(this IFileStorage storage, string path, CancellationToken cancellationToken = default) { - public static Task SaveObjectAsync(this IFileStorage storage, string path, T data, CancellationToken cancellationToken = default) - { - if (String.IsNullOrEmpty(path)) - throw new ArgumentNullException(nameof(path)); + if (String.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); - var bytes = storage.Serializer.SerializeToBytes(data); - return storage.SaveFileAsync(path, new MemoryStream(bytes), cancellationToken); - } + using var stream = await storage.GetFileStreamAsync(path, cancellationToken).AnyContext(); + if (stream != null) + return storage.Serializer.Deserialize(stream); - public static async Task GetObjectAsync(this IFileStorage storage, string path, CancellationToken cancellationToken = default) - { - if (String.IsNullOrEmpty(path)) - throw new ArgumentNullException(nameof(path)); + return default; + } - using var stream = await storage.GetFileStreamAsync(path, cancellationToken).AnyContext(); - if (stream != null) - return storage.Serializer.Deserialize(stream); + public static async Task DeleteFilesAsync(this IFileStorage storage, IEnumerable files) + { + if (files == null) + throw new ArgumentNullException(nameof(files)); - return default; - } + foreach (var file in files) + await storage.DeleteFileAsync(file.Path).AnyContext(); + } - public static async Task DeleteFilesAsync(this IFileStorage storage, IEnumerable files) - { - if (files == null) - throw new ArgumentNullException(nameof(files)); + public static async Task GetFileContentsAsync(this IFileStorage storage, string path) + { + if (String.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); - foreach (var file in files) - await storage.DeleteFileAsync(file.Path).AnyContext(); - } + using var stream = await storage.GetFileStreamAsync(path).AnyContext(); + if (stream != null) + return await new StreamReader(stream).ReadToEndAsync().AnyContext(); - public static async Task GetFileContentsAsync(this IFileStorage storage, string path) - { - if (String.IsNullOrEmpty(path)) - throw new ArgumentNullException(nameof(path)); + return null; + } - using var stream = await storage.GetFileStreamAsync(path).AnyContext(); - if (stream != null) - return await new StreamReader(stream).ReadToEndAsync().AnyContext(); + public static async Task GetFileContentsRawAsync(this IFileStorage storage, string path) + { + if (String.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); + using var stream = await storage.GetFileStreamAsync(path).AnyContext(); + if (stream == null) return null; - } - public static async Task GetFileContentsRawAsync(this IFileStorage storage, string path) + var buffer = new byte[16 * 1024]; + using var ms = new MemoryStream(); + int read; + while ((read = await stream.ReadAsync(buffer, 0, buffer.Length).AnyContext()) > 0) { - if (String.IsNullOrEmpty(path)) - throw new ArgumentNullException(nameof(path)); - - using var stream = await storage.GetFileStreamAsync(path).AnyContext(); - if (stream == null) - return null; - - var buffer = new byte[16 * 1024]; - using var ms = new MemoryStream(); - int read; - while ((read = await stream.ReadAsync(buffer, 0, buffer.Length).AnyContext()) > 0) - { - await ms.WriteAsync(buffer, 0, read).AnyContext(); - } - - return ms.ToArray(); + await ms.WriteAsync(buffer, 0, read).AnyContext(); } - public static Task SaveFileAsync(this IFileStorage storage, string path, string contents) - { - if (String.IsNullOrEmpty(path)) - throw new ArgumentNullException(nameof(path)); + return ms.ToArray(); + } - return storage.SaveFileAsync(path, new MemoryStream(Encoding.UTF8.GetBytes(contents ?? String.Empty))); - } + public static Task SaveFileAsync(this IFileStorage storage, string path, string contents) + { + if (String.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); + + return storage.SaveFileAsync(path, new MemoryStream(Encoding.UTF8.GetBytes(contents ?? String.Empty))); + } - public static async Task> GetFileListAsync(this IFileStorage storage, string searchPattern = null, int? limit = null, CancellationToken cancellationToken = default) + public static async Task> GetFileListAsync(this IFileStorage storage, string searchPattern = null, int? limit = null, CancellationToken cancellationToken = default) + { + var files = new List(); + limit ??= Int32.MaxValue; + var result = await storage.GetPagedFileListAsync(limit.Value, searchPattern, cancellationToken).AnyContext(); + do { - var files = new List(); - limit ??= Int32.MaxValue; - var result = await storage.GetPagedFileListAsync(limit.Value, searchPattern, cancellationToken).AnyContext(); - do - { - files.AddRange(result.Files); - } while (result.HasMore && files.Count < limit.Value && await result.NextPageAsync().AnyContext()); - - return files; - } + files.AddRange(result.Files); + } while (result.HasMore && files.Count < limit.Value && await result.NextPageAsync().AnyContext()); + + return files; } } diff --git a/src/Foundatio/Storage/InMemoryFileStorage.cs b/src/Foundatio/Storage/InMemoryFileStorage.cs index 6d8023d4f..cc8fde1ab 100644 --- a/src/Foundatio/Storage/InMemoryFileStorage.cs +++ b/src/Foundatio/Storage/InMemoryFileStorage.cs @@ -12,293 +12,292 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -namespace Foundatio.Storage +namespace Foundatio.Storage; + +public class InMemoryFileStorage : IFileStorage { - public class InMemoryFileStorage : IFileStorage - { - private readonly Dictionary> _storage = new(StringComparer.OrdinalIgnoreCase); - private readonly AsyncLock _lock = new(); - private readonly ISerializer _serializer; - protected readonly ILogger _logger; + private readonly Dictionary> _storage = new(StringComparer.OrdinalIgnoreCase); + private readonly AsyncLock _lock = new(); + private readonly ISerializer _serializer; + protected readonly ILogger _logger; - public InMemoryFileStorage() : this(o => o) { } + public InMemoryFileStorage() : this(o => o) { } - public InMemoryFileStorage(InMemoryFileStorageOptions options) - { - if (options == null) - throw new ArgumentNullException(nameof(options)); + public InMemoryFileStorage(InMemoryFileStorageOptions options) + { + if (options == null) + throw new ArgumentNullException(nameof(options)); - MaxFileSize = options.MaxFileSize; - MaxFiles = options.MaxFiles; - _serializer = options.Serializer ?? DefaultSerializer.Instance; - _logger = options.LoggerFactory?.CreateLogger(GetType()) ?? NullLogger.Instance; - } + MaxFileSize = options.MaxFileSize; + MaxFiles = options.MaxFiles; + _serializer = options.Serializer ?? DefaultSerializer.Instance; + _logger = options.LoggerFactory?.CreateLogger(GetType()) ?? NullLogger.Instance; + } - public InMemoryFileStorage(Builder config) - : this(config(new InMemoryFileStorageOptionsBuilder()).Build()) { } + public InMemoryFileStorage(Builder config) + : this(config(new InMemoryFileStorageOptionsBuilder()).Build()) { } - public long MaxFileSize { get; set; } - public long MaxFiles { get; set; } - ISerializer IHaveSerializer.Serializer => _serializer; + public long MaxFileSize { get; set; } + public long MaxFiles { get; set; } + ISerializer IHaveSerializer.Serializer => _serializer; - [Obsolete($"Use {nameof(GetFileStreamAsync)} with {nameof(FileAccess)} instead to define read or write behaviour of stream")] - public Task GetFileStreamAsync(string path, CancellationToken cancellationToken = default) => - GetFileStreamAsync(path, StreamMode.Read, cancellationToken); + [Obsolete($"Use {nameof(GetFileStreamAsync)} with {nameof(FileAccess)} instead to define read or write behaviour of stream")] + public Task GetFileStreamAsync(string path, CancellationToken cancellationToken = default) => + GetFileStreamAsync(path, StreamMode.Read, cancellationToken); - public async Task GetFileStreamAsync(string path, StreamMode streamMode, CancellationToken cancellationToken = default) - { - if (String.IsNullOrEmpty(path)) - throw new ArgumentNullException(nameof(path)); + public async Task GetFileStreamAsync(string path, StreamMode streamMode, CancellationToken cancellationToken = default) + { + if (String.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); - string normalizedPath = path.NormalizePath(); - _logger.LogTrace("Getting file stream for {Path}", normalizedPath); + string normalizedPath = path.NormalizePath(); + _logger.LogTrace("Getting file stream for {Path}", normalizedPath); - using (await _lock.LockAsync().AnyContext()) + using (await _lock.LockAsync().AnyContext()) + { + if (!_storage.ContainsKey(normalizedPath)) { - if (!_storage.ContainsKey(normalizedPath)) - { - _logger.LogError("Unable to get file stream for {Path}: File Not Found", normalizedPath); - return null; - } - - return new MemoryStream(_storage[normalizedPath].Item2); + _logger.LogError("Unable to get file stream for {Path}: File Not Found", normalizedPath); + return null; } - } - public async Task GetFileInfoAsync(string path) - { - if (String.IsNullOrEmpty(path)) - throw new ArgumentNullException(nameof(path)); + return new MemoryStream(_storage[normalizedPath].Item2); + } + } - string normalizedPath = path.NormalizePath(); - _logger.LogTrace("Getting file info for {Path}", normalizedPath); + public async Task GetFileInfoAsync(string path) + { + if (String.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); - if (await ExistsAsync(normalizedPath).AnyContext()) - return _storage[normalizedPath].Item1.DeepClone(); + string normalizedPath = path.NormalizePath(); + _logger.LogTrace("Getting file info for {Path}", normalizedPath); - _logger.LogError("Unable to get file info for {Path}: File Not Found", normalizedPath); - return null; - } + if (await ExistsAsync(normalizedPath).AnyContext()) + return _storage[normalizedPath].Item1.DeepClone(); - public async Task ExistsAsync(string path) - { - if (String.IsNullOrEmpty(path)) - throw new ArgumentNullException(nameof(path)); + _logger.LogError("Unable to get file info for {Path}: File Not Found", normalizedPath); + return null; + } - string normalizedPath = path.NormalizePath(); - _logger.LogTrace("Checking if {Path} exists", normalizedPath); + public async Task ExistsAsync(string path) + { + if (String.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); - using (await _lock.LockAsync().AnyContext()) - { - return _storage.ContainsKey(normalizedPath); - } - } + string normalizedPath = path.NormalizePath(); + _logger.LogTrace("Checking if {Path} exists", normalizedPath); - private static byte[] ReadBytes(Stream input) + using (await _lock.LockAsync().AnyContext()) { - using var ms = new MemoryStream(); - input.CopyTo(ms); - return ms.ToArray(); + return _storage.ContainsKey(normalizedPath); } + } - public async Task SaveFileAsync(string path, Stream stream, CancellationToken cancellationToken = default) - { - if (String.IsNullOrEmpty(path)) - throw new ArgumentNullException(nameof(path)); - if (stream == null) - throw new ArgumentNullException(nameof(stream)); + private static byte[] ReadBytes(Stream input) + { + using var ms = new MemoryStream(); + input.CopyTo(ms); + return ms.ToArray(); + } - string normalizedPath = path.NormalizePath(); - _logger.LogTrace("Saving {Path}", normalizedPath); + public async Task SaveFileAsync(string path, Stream stream, CancellationToken cancellationToken = default) + { + if (String.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); + if (stream == null) + throw new ArgumentNullException(nameof(stream)); - var contents = ReadBytes(stream); - if (contents.Length > MaxFileSize) - throw new ArgumentException($"File size {contents.Length.ToFileSizeDisplay()} exceeds the maximum size of {MaxFileSize.ToFileSizeDisplay()}."); + string normalizedPath = path.NormalizePath(); + _logger.LogTrace("Saving {Path}", normalizedPath); - using (await _lock.LockAsync().AnyContext()) - { - _storage[normalizedPath] = Tuple.Create(new FileSpec - { - Created = SystemClock.UtcNow, - Modified = SystemClock.UtcNow, - Path = normalizedPath, - Size = contents.Length - }, contents); - - if (_storage.Count > MaxFiles) - _storage.Remove(_storage.OrderByDescending(kvp => kvp.Value.Item1.Created).First().Key); - } + var contents = ReadBytes(stream); + if (contents.Length > MaxFileSize) + throw new ArgumentException($"File size {contents.Length.ToFileSizeDisplay()} exceeds the maximum size of {MaxFileSize.ToFileSizeDisplay()}."); - return true; + using (await _lock.LockAsync().AnyContext()) + { + _storage[normalizedPath] = Tuple.Create(new FileSpec + { + Created = SystemClock.UtcNow, + Modified = SystemClock.UtcNow, + Path = normalizedPath, + Size = contents.Length + }, contents); + + if (_storage.Count > MaxFiles) + _storage.Remove(_storage.OrderByDescending(kvp => kvp.Value.Item1.Created).First().Key); } - public async Task RenameFileAsync(string path, string newPath, CancellationToken cancellationToken = default) - { - if (String.IsNullOrEmpty(path)) - throw new ArgumentNullException(nameof(path)); - if (String.IsNullOrEmpty(newPath)) - throw new ArgumentNullException(nameof(newPath)); + return true; + } + + public async Task RenameFileAsync(string path, string newPath, CancellationToken cancellationToken = default) + { + if (String.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); + if (String.IsNullOrEmpty(newPath)) + throw new ArgumentNullException(nameof(newPath)); - string normalizedPath = path.NormalizePath(); - string normalizedNewPath = newPath.NormalizePath(); - _logger.LogInformation("Renaming {Path} to {NewPath}", normalizedPath, normalizedNewPath); + string normalizedPath = path.NormalizePath(); + string normalizedNewPath = newPath.NormalizePath(); + _logger.LogInformation("Renaming {Path} to {NewPath}", normalizedPath, normalizedNewPath); - using (await _lock.LockAsync().AnyContext()) + using (await _lock.LockAsync().AnyContext()) + { + if (!_storage.ContainsKey(normalizedPath)) { - if (!_storage.ContainsKey(normalizedPath)) - { - _logger.LogDebug("Error renaming {Path} to {NewPath}: File not found", normalizedPath, normalizedNewPath); - return false; - } - - _storage[normalizedNewPath] = _storage[normalizedPath]; - _storage[normalizedNewPath].Item1.Path = normalizedNewPath; - _storage[normalizedNewPath].Item1.Modified = SystemClock.UtcNow; - _storage.Remove(normalizedPath); + _logger.LogDebug("Error renaming {Path} to {NewPath}: File not found", normalizedPath, normalizedNewPath); + return false; } - return true; + _storage[normalizedNewPath] = _storage[normalizedPath]; + _storage[normalizedNewPath].Item1.Path = normalizedNewPath; + _storage[normalizedNewPath].Item1.Modified = SystemClock.UtcNow; + _storage.Remove(normalizedPath); } - public async Task CopyFileAsync(string path, string targetPath, CancellationToken cancellationToken = default) - { - if (String.IsNullOrEmpty(path)) - throw new ArgumentNullException(nameof(path)); - if (String.IsNullOrEmpty(targetPath)) - throw new ArgumentNullException(nameof(targetPath)); + return true; + } - string normalizedPath = path.NormalizePath(); - string normalizedTargetPath = targetPath.NormalizePath(); - _logger.LogInformation("Copying {Path} to {TargetPath}", normalizedPath, normalizedTargetPath); + public async Task CopyFileAsync(string path, string targetPath, CancellationToken cancellationToken = default) + { + if (String.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); + if (String.IsNullOrEmpty(targetPath)) + throw new ArgumentNullException(nameof(targetPath)); - using (await _lock.LockAsync().AnyContext()) + string normalizedPath = path.NormalizePath(); + string normalizedTargetPath = targetPath.NormalizePath(); + _logger.LogInformation("Copying {Path} to {TargetPath}", normalizedPath, normalizedTargetPath); + + using (await _lock.LockAsync().AnyContext()) + { + if (!_storage.ContainsKey(normalizedPath)) { - if (!_storage.ContainsKey(normalizedPath)) - { - _logger.LogDebug("Error copying {Path} to {TargetPath}: File not found", normalizedPath, normalizedTargetPath); - return false; - } - - _storage[normalizedTargetPath] = _storage[normalizedPath]; - _storage[normalizedTargetPath].Item1.Path = normalizedTargetPath; - _storage[normalizedTargetPath].Item1.Modified = SystemClock.UtcNow; + _logger.LogDebug("Error copying {Path} to {TargetPath}: File not found", normalizedPath, normalizedTargetPath); + return false; } - return true; + _storage[normalizedTargetPath] = _storage[normalizedPath]; + _storage[normalizedTargetPath].Item1.Path = normalizedTargetPath; + _storage[normalizedTargetPath].Item1.Modified = SystemClock.UtcNow; } - public async Task DeleteFileAsync(string path, CancellationToken cancellationToken = default) - { - if (String.IsNullOrEmpty(path)) - throw new ArgumentNullException(nameof(path)); + return true; + } - string normalizedPath = path.NormalizePath(); - _logger.LogTrace("Deleting {Path}", normalizedPath); + public async Task DeleteFileAsync(string path, CancellationToken cancellationToken = default) + { + if (String.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); - using (await _lock.LockAsync().AnyContext()) - { - if (!_storage.ContainsKey(normalizedPath)) - { - _logger.LogError("Unable to delete {Path}: File not found", normalizedPath); - return false; - } + string normalizedPath = path.NormalizePath(); + _logger.LogTrace("Deleting {Path}", normalizedPath); - _storage.Remove(normalizedPath); + using (await _lock.LockAsync().AnyContext()) + { + if (!_storage.ContainsKey(normalizedPath)) + { + _logger.LogError("Unable to delete {Path}: File not found", normalizedPath); + return false; } - return true; + _storage.Remove(normalizedPath); } - public async Task DeleteFilesAsync(string searchPattern = null, CancellationToken cancellation = default) + return true; + } + + public async Task DeleteFilesAsync(string searchPattern = null, CancellationToken cancellation = default) + { + if (String.IsNullOrEmpty(searchPattern) || searchPattern == "*") { - if (String.IsNullOrEmpty(searchPattern) || searchPattern == "*") + using (await _lock.LockAsync().AnyContext()) { - using (await _lock.LockAsync().AnyContext()) - { - _storage.Clear(); - } - - return 0; + _storage.Clear(); } - searchPattern = searchPattern.NormalizePath(); - int count = 0; + return 0; + } - if (searchPattern[searchPattern.Length - 1] == Path.DirectorySeparatorChar) - searchPattern = $"{searchPattern}*"; - else if (!searchPattern.EndsWith(Path.DirectorySeparatorChar + "*") && !Path.HasExtension(searchPattern)) - searchPattern = Path.Combine(searchPattern, "*"); + searchPattern = searchPattern.NormalizePath(); + int count = 0; - var regex = new Regex($"^{Regex.Escape(searchPattern).Replace("\\*", ".*?")}$"); + if (searchPattern[searchPattern.Length - 1] == Path.DirectorySeparatorChar) + searchPattern = $"{searchPattern}*"; + else if (!searchPattern.EndsWith(Path.DirectorySeparatorChar + "*") && !Path.HasExtension(searchPattern)) + searchPattern = Path.Combine(searchPattern, "*"); - using (await _lock.LockAsync().AnyContext()) - { - var keys = _storage.Keys.Where(k => regex.IsMatch(k)).Select(k => _storage[k].Item1).ToList(); + var regex = new Regex($"^{Regex.Escape(searchPattern).Replace("\\*", ".*?")}$"); - _logger.LogInformation("Deleting {FileCount} files matching {SearchPattern} (Regex={SearchPatternRegex})", keys.Count, searchPattern, regex); - foreach (var key in keys) - { - _logger.LogTrace("Deleting {Path}", key.Path); - _storage.Remove(key.Path); - count++; - } + using (await _lock.LockAsync().AnyContext()) + { + var keys = _storage.Keys.Where(k => regex.IsMatch(k)).Select(k => _storage[k].Item1).ToList(); - _logger.LogTrace("Finished deleting {FileCount} files matching {SearchPattern}", count, searchPattern); + _logger.LogInformation("Deleting {FileCount} files matching {SearchPattern} (Regex={SearchPatternRegex})", keys.Count, searchPattern, regex); + foreach (var key in keys) + { + _logger.LogTrace("Deleting {Path}", key.Path); + _storage.Remove(key.Path); + count++; } - return count; + _logger.LogTrace("Finished deleting {FileCount} files matching {SearchPattern}", count, searchPattern); } - public async Task GetPagedFileListAsync(int pageSize = 100, string searchPattern = null, CancellationToken cancellationToken = default) - { - if (pageSize <= 0) - return PagedFileListResult.Empty; - - if (String.IsNullOrEmpty(searchPattern)) - searchPattern = "*"; + return count; + } - searchPattern = searchPattern.NormalizePath(); + public async Task GetPagedFileListAsync(int pageSize = 100, string searchPattern = null, CancellationToken cancellationToken = default) + { + if (pageSize <= 0) + return PagedFileListResult.Empty; - var result = new PagedFileListResult(async s => await GetFilesAsync(searchPattern, 1, pageSize, cancellationToken)); - await result.NextPageAsync().AnyContext(); - return result; - } + if (String.IsNullOrEmpty(searchPattern)) + searchPattern = "*"; - private async Task GetFilesAsync(string searchPattern, int page, int pageSize, CancellationToken cancellationToken = default) - { - var list = new List(); - int pagingLimit = pageSize; - int skip = (page - 1) * pagingLimit; - if (pagingLimit < Int32.MaxValue) - pagingLimit++; + searchPattern = searchPattern.NormalizePath(); - var regex = new Regex($"^{Regex.Escape(searchPattern).Replace("\\*", ".*?")}$"); + var result = new PagedFileListResult(async s => await GetFilesAsync(searchPattern, 1, pageSize, cancellationToken)); + await result.NextPageAsync().AnyContext(); + return result; + } - using (await _lock.LockAsync().AnyContext()) - { - _logger.LogTrace(s => s.Property("Limit", pagingLimit).Property("Skip", skip), "Getting file list matching {SearchPattern}...", regex); - list.AddRange(_storage.Keys.Where(k => regex.IsMatch(k)).Select(k => _storage[k].Item1.DeepClone()).Skip(skip).Take(pagingLimit).ToList()); - } + private async Task GetFilesAsync(string searchPattern, int page, int pageSize, CancellationToken cancellationToken = default) + { + var list = new List(); + int pagingLimit = pageSize; + int skip = (page - 1) * pagingLimit; + if (pagingLimit < Int32.MaxValue) + pagingLimit++; - bool hasMore = false; - if (list.Count == pagingLimit) - { - hasMore = true; - list.RemoveAt(pagingLimit - 1); - } + var regex = new Regex($"^{Regex.Escape(searchPattern).Replace("\\*", ".*?")}$"); - return new NextPageResult - { - Success = true, - HasMore = hasMore, - Files = list, - NextPageFunc = hasMore ? async _ => await GetFilesAsync(searchPattern, page + 1, pageSize, cancellationToken) : null - }; + using (await _lock.LockAsync().AnyContext()) + { + _logger.LogTrace(s => s.Property("Limit", pagingLimit).Property("Skip", skip), "Getting file list matching {SearchPattern}...", regex); + list.AddRange(_storage.Keys.Where(k => regex.IsMatch(k)).Select(k => _storage[k].Item1.DeepClone()).Skip(skip).Take(pagingLimit).ToList()); } - public void Dispose() + bool hasMore = false; + if (list.Count == pagingLimit) { - _storage?.Clear(); + hasMore = true; + list.RemoveAt(pagingLimit - 1); } + + return new NextPageResult + { + Success = true, + HasMore = hasMore, + Files = list, + NextPageFunc = hasMore ? async _ => await GetFilesAsync(searchPattern, page + 1, pageSize, cancellationToken) : null + }; + } + + public void Dispose() + { + _storage?.Clear(); } } diff --git a/src/Foundatio/Storage/InMemoryFileStorageOptions.cs b/src/Foundatio/Storage/InMemoryFileStorageOptions.cs index 5609c3e9d..d1a4cd05f 100644 --- a/src/Foundatio/Storage/InMemoryFileStorageOptions.cs +++ b/src/Foundatio/Storage/InMemoryFileStorageOptions.cs @@ -1,23 +1,22 @@ -namespace Foundatio.Storage +namespace Foundatio.Storage; + +public class InMemoryFileStorageOptions : SharedOptions { - public class InMemoryFileStorageOptions : SharedOptions + public long MaxFileSize { get; set; } = 1024 * 1024 * 256; + public int MaxFiles { get; set; } = 100; +} + +public class InMemoryFileStorageOptionsBuilder : SharedOptionsBuilder +{ + public InMemoryFileStorageOptionsBuilder MaxFileSize(long maxFileSize) { - public long MaxFileSize { get; set; } = 1024 * 1024 * 256; - public int MaxFiles { get; set; } = 100; + Target.MaxFileSize = maxFileSize; + return this; } - public class InMemoryFileStorageOptionsBuilder : SharedOptionsBuilder + public InMemoryFileStorageOptionsBuilder MaxFiles(int maxFiles) { - public InMemoryFileStorageOptionsBuilder MaxFileSize(long maxFileSize) - { - Target.MaxFileSize = maxFileSize; - return this; - } - - public InMemoryFileStorageOptionsBuilder MaxFiles(int maxFiles) - { - Target.MaxFiles = maxFiles; - return this; - } + Target.MaxFiles = maxFiles; + return this; } } diff --git a/src/Foundatio/Storage/ScopedFileStorage.cs b/src/Foundatio/Storage/ScopedFileStorage.cs index bf4e6fd36..8964e9a83 100644 --- a/src/Foundatio/Storage/ScopedFileStorage.cs +++ b/src/Foundatio/Storage/ScopedFileStorage.cs @@ -5,131 +5,130 @@ using Foundatio.Serializer; using Foundatio.Utility; -namespace Foundatio.Storage -{ - public class ScopedFileStorage : IFileStorage - { - private readonly string _pathPrefix; +namespace Foundatio.Storage; - public ScopedFileStorage(IFileStorage storage, string scope) - { - UnscopedStorage = storage; - Scope = !String.IsNullOrWhiteSpace(scope) ? scope.Trim() : null; - _pathPrefix = Scope != null ? String.Concat(Scope, "/") : String.Empty; - } +public class ScopedFileStorage : IFileStorage +{ + private readonly string _pathPrefix; - public IFileStorage UnscopedStorage { get; private set; } + public ScopedFileStorage(IFileStorage storage, string scope) + { + UnscopedStorage = storage; + Scope = !String.IsNullOrWhiteSpace(scope) ? scope.Trim() : null; + _pathPrefix = Scope != null ? String.Concat(Scope, "/") : String.Empty; + } - public string Scope { get; private set; } - ISerializer IHaveSerializer.Serializer => UnscopedStorage.Serializer; + public IFileStorage UnscopedStorage { get; private set; } - [Obsolete($"Use {nameof(GetFileStreamAsync)} with {nameof(FileAccess)} instead to define read or write behaviour of stream")] - public Task GetFileStreamAsync(string path, CancellationToken cancellationToken = default) - => GetFileStreamAsync(path, StreamMode.Read, cancellationToken); + public string Scope { get; private set; } + ISerializer IHaveSerializer.Serializer => UnscopedStorage.Serializer; - public Task GetFileStreamAsync(string path, StreamMode streamMode, CancellationToken cancellationToken = default) - { - if (String.IsNullOrEmpty(path)) - throw new ArgumentNullException(nameof(path)); + [Obsolete($"Use {nameof(GetFileStreamAsync)} with {nameof(FileAccess)} instead to define read or write behaviour of stream")] + public Task GetFileStreamAsync(string path, CancellationToken cancellationToken = default) + => GetFileStreamAsync(path, StreamMode.Read, cancellationToken); - return UnscopedStorage.GetFileStreamAsync(String.Concat(_pathPrefix, path), cancellationToken); - } + public Task GetFileStreamAsync(string path, StreamMode streamMode, CancellationToken cancellationToken = default) + { + if (String.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); - public async Task GetFileInfoAsync(string path) - { - if (String.IsNullOrEmpty(path)) - throw new ArgumentNullException(nameof(path)); + return UnscopedStorage.GetFileStreamAsync(String.Concat(_pathPrefix, path), cancellationToken); + } - var file = await UnscopedStorage.GetFileInfoAsync(String.Concat(_pathPrefix, path)).AnyContext(); - if (file != null) - file.Path = file.Path.Substring(_pathPrefix.Length); + public async Task GetFileInfoAsync(string path) + { + if (String.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); - return file; - } + var file = await UnscopedStorage.GetFileInfoAsync(String.Concat(_pathPrefix, path)).AnyContext(); + if (file != null) + file.Path = file.Path.Substring(_pathPrefix.Length); - public Task ExistsAsync(string path) - { - if (String.IsNullOrEmpty(path)) - throw new ArgumentNullException(nameof(path)); + return file; + } - return UnscopedStorage.ExistsAsync(String.Concat(_pathPrefix, path)); - } + public Task ExistsAsync(string path) + { + if (String.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); - public Task SaveFileAsync(string path, Stream stream, CancellationToken cancellationToken = default) - { - if (String.IsNullOrEmpty(path)) - throw new ArgumentNullException(nameof(path)); + return UnscopedStorage.ExistsAsync(String.Concat(_pathPrefix, path)); + } - if (stream == null) - throw new ArgumentNullException(nameof(stream)); + public Task SaveFileAsync(string path, Stream stream, CancellationToken cancellationToken = default) + { + if (String.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); - return UnscopedStorage.SaveFileAsync(String.Concat(_pathPrefix, path), stream, cancellationToken); - } + if (stream == null) + throw new ArgumentNullException(nameof(stream)); - public Task RenameFileAsync(string path, string newPath, CancellationToken cancellationToken = default) - { - if (String.IsNullOrEmpty(path)) - throw new ArgumentNullException(nameof(path)); - if (String.IsNullOrEmpty(newPath)) - throw new ArgumentNullException(nameof(newPath)); + return UnscopedStorage.SaveFileAsync(String.Concat(_pathPrefix, path), stream, cancellationToken); + } - return UnscopedStorage.RenameFileAsync(String.Concat(_pathPrefix, path), String.Concat(_pathPrefix, newPath), cancellationToken); - } + public Task RenameFileAsync(string path, string newPath, CancellationToken cancellationToken = default) + { + if (String.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); + if (String.IsNullOrEmpty(newPath)) + throw new ArgumentNullException(nameof(newPath)); - public Task CopyFileAsync(string path, string targetPath, CancellationToken cancellationToken = default) - { - if (String.IsNullOrEmpty(path)) - throw new ArgumentNullException(nameof(path)); - if (String.IsNullOrEmpty(targetPath)) - throw new ArgumentNullException(nameof(targetPath)); + return UnscopedStorage.RenameFileAsync(String.Concat(_pathPrefix, path), String.Concat(_pathPrefix, newPath), cancellationToken); + } - return UnscopedStorage.CopyFileAsync(String.Concat(_pathPrefix, path), String.Concat(_pathPrefix, targetPath), cancellationToken); - } + public Task CopyFileAsync(string path, string targetPath, CancellationToken cancellationToken = default) + { + if (String.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); + if (String.IsNullOrEmpty(targetPath)) + throw new ArgumentNullException(nameof(targetPath)); - public Task DeleteFileAsync(string path, CancellationToken cancellationToken = default) - { - if (String.IsNullOrEmpty(path)) - throw new ArgumentNullException(nameof(path)); + return UnscopedStorage.CopyFileAsync(String.Concat(_pathPrefix, path), String.Concat(_pathPrefix, targetPath), cancellationToken); + } - return UnscopedStorage.DeleteFileAsync(String.Concat(_pathPrefix, path), cancellationToken); - } + public Task DeleteFileAsync(string path, CancellationToken cancellationToken = default) + { + if (String.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); - public Task DeleteFilesAsync(string searchPattern = null, CancellationToken cancellation = default) - { - searchPattern = !String.IsNullOrEmpty(searchPattern) ? String.Concat(_pathPrefix, searchPattern) : String.Concat(_pathPrefix, "*"); - return UnscopedStorage.DeleteFilesAsync(searchPattern, cancellation); - } + return UnscopedStorage.DeleteFileAsync(String.Concat(_pathPrefix, path), cancellationToken); + } - public async Task GetPagedFileListAsync(int pageSize = 100, string searchPattern = null, CancellationToken cancellationToken = default) - { - if (pageSize <= 0) - return PagedFileListResult.Empty; + public Task DeleteFilesAsync(string searchPattern = null, CancellationToken cancellation = default) + { + searchPattern = !String.IsNullOrEmpty(searchPattern) ? String.Concat(_pathPrefix, searchPattern) : String.Concat(_pathPrefix, "*"); + return UnscopedStorage.DeleteFilesAsync(searchPattern, cancellation); + } - searchPattern = !String.IsNullOrEmpty(searchPattern) ? String.Concat(_pathPrefix, searchPattern) : String.Concat(_pathPrefix, "*"); - var unscopedResult = await UnscopedStorage.GetPagedFileListAsync(pageSize, searchPattern, cancellationToken).AnyContext(); + public async Task GetPagedFileListAsync(int pageSize = 100, string searchPattern = null, CancellationToken cancellationToken = default) + { + if (pageSize <= 0) + return PagedFileListResult.Empty; - foreach (var file in unscopedResult.Files) - file.Path = file.Path.Substring(_pathPrefix.Length); + searchPattern = !String.IsNullOrEmpty(searchPattern) ? String.Concat(_pathPrefix, searchPattern) : String.Concat(_pathPrefix, "*"); + var unscopedResult = await UnscopedStorage.GetPagedFileListAsync(pageSize, searchPattern, cancellationToken).AnyContext(); - return new PagedFileListResult(unscopedResult.Files, unscopedResult.HasMore, unscopedResult.HasMore ? _ => NextPage(unscopedResult) : null); - } + foreach (var file in unscopedResult.Files) + file.Path = file.Path.Substring(_pathPrefix.Length); - private async Task NextPage(PagedFileListResult result) - { - var success = await result.NextPageAsync().AnyContext(); + return new PagedFileListResult(unscopedResult.Files, unscopedResult.HasMore, unscopedResult.HasMore ? _ => NextPage(unscopedResult) : null); + } - foreach (var file in result.Files) - file.Path = file.Path.Substring(_pathPrefix.Length); + private async Task NextPage(PagedFileListResult result) + { + var success = await result.NextPageAsync().AnyContext(); - return new NextPageResult - { - Success = success, - HasMore = result.HasMore, - Files = result.Files, - NextPageFunc = s => NextPage(result) - }; - } + foreach (var file in result.Files) + file.Path = file.Path.Substring(_pathPrefix.Length); - public void Dispose() { } + return new NextPageResult + { + Success = success, + HasMore = result.HasMore, + Files = result.Files, + NextPageFunc = s => NextPage(result) + }; } + + public void Dispose() { } } diff --git a/src/Foundatio/Utility/AsyncEvent.cs b/src/Foundatio/Utility/AsyncEvent.cs index f157a9695..78739c843 100644 --- a/src/Foundatio/Utility/AsyncEvent.cs +++ b/src/Foundatio/Utility/AsyncEvent.cs @@ -3,90 +3,89 @@ using System.Linq; using System.Threading.Tasks; -namespace Foundatio.Utility +namespace Foundatio.Utility; + +public class AsyncEvent : IObservable, IDisposable where TEventArgs : EventArgs { - public class AsyncEvent : IObservable, IDisposable where TEventArgs : EventArgs + private readonly List> _invocationList = new(); + private readonly object _lockObject = new(); + private readonly bool _parallelInvoke; + + public AsyncEvent(bool parallelInvoke = false) { - private readonly List> _invocationList = new(); - private readonly object _lockObject = new(); - private readonly bool _parallelInvoke; + _parallelInvoke = parallelInvoke; + } - public AsyncEvent(bool parallelInvoke = false) - { - _parallelInvoke = parallelInvoke; - } + public bool HasHandlers => _invocationList.Count > 0; + + public IDisposable AddHandler(Func callback) + { + if (callback == null) + throw new NullReferenceException("callback is null"); - public bool HasHandlers => _invocationList.Count > 0; + lock (_lockObject) + _invocationList.Add(callback); - public IDisposable AddHandler(Func callback) + return new EventHandlerDisposable(this, callback); + } + + public IDisposable AddSyncHandler(Action callback) + { + return AddHandler((sender, args) => { - if (callback == null) - throw new NullReferenceException("callback is null"); + callback(sender, args); + return Task.CompletedTask; + }); + } - lock (_lockObject) - _invocationList.Add(callback); + public void RemoveHandler(Func callback) + { + if (callback == null) + throw new NullReferenceException("callback is null"); - return new EventHandlerDisposable(this, callback); - } + lock (_lockObject) + _invocationList.Remove(callback); + } - public IDisposable AddSyncHandler(Action callback) - { - return AddHandler((sender, args) => - { - callback(sender, args); - return Task.CompletedTask; - }); - } + public async Task InvokeAsync(object sender, TEventArgs eventArgs) + { + List> tmpInvocationList; - public void RemoveHandler(Func callback) - { - if (callback == null) - throw new NullReferenceException("callback is null"); + lock (_lockObject) + tmpInvocationList = new List>(_invocationList); - lock (_lockObject) - _invocationList.Remove(callback); - } + if (_parallelInvoke) + await Task.WhenAll(tmpInvocationList.Select(callback => callback(sender, eventArgs))).AnyContext(); + else + foreach (var callback in tmpInvocationList) + await callback(sender, eventArgs).AnyContext(); + } - public async Task InvokeAsync(object sender, TEventArgs eventArgs) - { - List> tmpInvocationList; + public IDisposable Subscribe(IObserver observer) + { + return AddSyncHandler((sender, args) => observer.OnNext(args)); + } - lock (_lockObject) - tmpInvocationList = new List>(_invocationList); + public void Dispose() + { + lock (_lockObject) + _invocationList.Clear(); + } - if (_parallelInvoke) - await Task.WhenAll(tmpInvocationList.Select(callback => callback(sender, eventArgs))).AnyContext(); - else - foreach (var callback in tmpInvocationList) - await callback(sender, eventArgs).AnyContext(); - } + private class EventHandlerDisposable : IDisposable where T : EventArgs + { + private readonly AsyncEvent _event; + private readonly Func _callback; - public IDisposable Subscribe(IObserver observer) + public EventHandlerDisposable(AsyncEvent @event, Func callback) { - return AddSyncHandler((sender, args) => observer.OnNext(args)); + _event = @event; + _callback = callback; } public void Dispose() { - lock (_lockObject) - _invocationList.Clear(); - } - - private class EventHandlerDisposable : IDisposable where T : EventArgs - { - private readonly AsyncEvent _event; - private readonly Func _callback; - - public EventHandlerDisposable(AsyncEvent @event, Func callback) - { - _event = @event; - _callback = callback; - } - - public void Dispose() - { - _event.RemoveHandler(_callback); - } + _event.RemoveHandler(_callback); } } } diff --git a/src/Foundatio/Utility/ConnectionStringParser.cs b/src/Foundatio/Utility/ConnectionStringParser.cs index 68397490d..15e6aadb3 100644 --- a/src/Foundatio/Utility/ConnectionStringParser.cs +++ b/src/Foundatio/Utility/ConnectionStringParser.cs @@ -7,12 +7,12 @@ using System.Text.RegularExpressions; using Foundatio.Utility; -namespace Foundatio.Utility +namespace Foundatio.Utility; + +public static class ConnectionStringParser { - public static class ConnectionStringParser - { - // borrowed from https://github.com/dotnet/corefx/blob/release/2.2/src/Common/src/System/Data/Common/DbConnectionOptions.Common.cs - private const string ConnectionStringPattern = // may not contain embedded null except trailing last value + // borrowed from https://github.com/dotnet/corefx/blob/release/2.2/src/Common/src/System/Data/Common/DbConnectionOptions.Common.cs + private const string ConnectionStringPattern = // may not contain embedded null except trailing last value "([\\s;]*" // leading whitespace and extra semicolons + "(?![\\s;])" // key does not start with space or semicolon + "(?([^=\\s\\p{Cc}]|\\s+[^=\\s\\p{Cc}]|\\s+==|==)+)" // allow any visible character for keyname except '=' which must quoted as '==' @@ -30,90 +30,89 @@ public static class ConnectionStringParser + "[\\s;]*[\u0000\\s]*" // trailing whitespace/semicolons (DataSourceLocator), embedded nulls are allowed only in the end ; - private static readonly Regex _connectionStringRegex = new(ConnectionStringPattern, RegexOptions.ExplicitCapture | RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace); + private static readonly Regex _connectionStringRegex = new(ConnectionStringPattern, RegexOptions.ExplicitCapture | RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace); - private static Dictionary Parse(string connectionString, IDictionary synonyms) - { - var parseTable = new Dictionary(StringComparer.OrdinalIgnoreCase); + private static Dictionary Parse(string connectionString, IDictionary synonyms) + { + var parseTable = new Dictionary(StringComparer.OrdinalIgnoreCase); - const int keyIndex = 1, valueIndex = 2; - Debug.Assert(keyIndex == _connectionStringRegex.GroupNumberFromName("key"), "wrong key index"); - Debug.Assert(valueIndex == _connectionStringRegex.GroupNumberFromName("value"), "wrong value index"); + const int keyIndex = 1, valueIndex = 2; + Debug.Assert(keyIndex == _connectionStringRegex.GroupNumberFromName("key"), "wrong key index"); + Debug.Assert(valueIndex == _connectionStringRegex.GroupNumberFromName("value"), "wrong value index"); - if (null == connectionString) - return parseTable; + if (null == connectionString) + return parseTable; - var match = _connectionStringRegex.Match(connectionString); - if (!match.Success || (match.Length != connectionString.Length)) - throw new ArgumentException($"Format of the initialization string does not conform to specification starting at index {match.Length}"); + var match = _connectionStringRegex.Match(connectionString); + if (!match.Success || (match.Length != connectionString.Length)) + throw new ArgumentException($"Format of the initialization string does not conform to specification starting at index {match.Length}"); - int indexValue = 0; - var keyValues = match.Groups[valueIndex].Captures; - foreach (Capture keyPair in match.Groups[keyIndex].Captures) + int indexValue = 0; + var keyValues = match.Groups[valueIndex].Captures; + foreach (Capture keyPair in match.Groups[keyIndex].Captures) + { + string keyName = keyPair.Value.Replace("==", "="); + string keyValue = keyValues[indexValue++].Value; + if (0 < keyValue.Length) { - string keyName = keyPair.Value.Replace("==", "="); - string keyValue = keyValues[indexValue++].Value; - if (0 < keyValue.Length) - { - switch (keyValue[0]) - { - case '\"': - keyValue = keyValue.Substring(1, keyValue.Length - 2).Replace("\"\"", "\""); - break; - case '\'': - keyValue = keyValue.Substring(1, keyValue.Length - 2).Replace("\'\'", "\'"); - break; - } - } - else + switch (keyValue[0]) { - keyValue = null; + case '\"': + keyValue = keyValue.Substring(1, keyValue.Length - 2).Replace("\"\"", "\""); + break; + case '\'': + keyValue = keyValue.Substring(1, keyValue.Length - 2).Replace("\'\'", "\'"); + break; } + } + else + { + keyValue = null; + } - string realKeyName = synonyms != null ? (synonyms.TryGetValue(keyName, out string synonym) ? synonym : null) : keyName; + string realKeyName = synonyms != null ? (synonyms.TryGetValue(keyName, out string synonym) ? synonym : null) : keyName; - if (!IsKeyNameValid(realKeyName)) - throw new ArgumentException($"Keyword not supported: '{keyName}'"); + if (!IsKeyNameValid(realKeyName)) + throw new ArgumentException($"Keyword not supported: '{keyName}'"); - if (!parseTable.ContainsKey(realKeyName)) - parseTable[realKeyName] = keyValue; // last key-value pair wins (or first) - } - - return parseTable; + if (!parseTable.ContainsKey(realKeyName)) + parseTable[realKeyName] = keyValue; // last key-value pair wins (or first) } - private static bool IsKeyNameValid(string keyName) - { - if (String.IsNullOrEmpty(keyName)) - return false; + return parseTable; + } - return keyName[0] != ';' && !Char.IsWhiteSpace(keyName[0]) && keyName.IndexOf('\u0000') == -1; - } + private static bool IsKeyNameValid(string keyName) + { + if (String.IsNullOrEmpty(keyName)) + return false; - public static Dictionary ParseConnectionString(this string connectionString, IDictionary synonyms = null) - { - return Parse(connectionString, synonyms); - } + return keyName[0] != ';' && !Char.IsWhiteSpace(keyName[0]) && keyName.IndexOf('\u0000') == -1; + } - public static string BuildConnectionString(this IDictionary options, IEnumerable excludedKeys = null) - { - if (options == null || options.Count == 0) - return null; + public static Dictionary ParseConnectionString(this string connectionString, IDictionary synonyms = null) + { + return Parse(connectionString, synonyms); + } - var excludes = new HashSet(excludedKeys ?? new string[] { }, StringComparer.OrdinalIgnoreCase); - var builder = new StringBuilder(); - foreach (var option in options) - { - if (excludes.Contains(option.Key)) - continue; + public static string BuildConnectionString(this IDictionary options, IEnumerable excludedKeys = null) + { + if (options == null || options.Count == 0) + return null; - if (option.Value != null && option.Value.Contains("\"")) - builder.Append($"{option.Key}=\"{option.Value.Replace("\"", "\"\"")}\";"); - else - builder.Append($"{option.Key}={option.Value};"); - } + var excludes = new HashSet(excludedKeys ?? new string[] { }, StringComparer.OrdinalIgnoreCase); + var builder = new StringBuilder(); + foreach (var option in options) + { + if (excludes.Contains(option.Key)) + continue; - return builder.ToString().TrimEnd(';'); + if (option.Value != null && option.Value.Contains("\"")) + builder.Append($"{option.Key}=\"{option.Value.Replace("\"", "\"\"")}\";"); + else + builder.Append($"{option.Key}={option.Value};"); } + + return builder.ToString().TrimEnd(';'); } } diff --git a/src/Foundatio/Utility/DataDictionary.cs b/src/Foundatio/Utility/DataDictionary.cs index 245066a11..ec60b483b 100644 --- a/src/Foundatio/Utility/DataDictionary.cs +++ b/src/Foundatio/Utility/DataDictionary.cs @@ -2,138 +2,137 @@ using System.Collections.Generic; using Foundatio.Serializer; -namespace Foundatio.Utility +namespace Foundatio.Utility; + +public interface IHaveData { - public interface IHaveData - { - IDictionary Data { get; } - } + IDictionary Data { get; } +} - public class DataDictionary : Dictionary - { - public static readonly DataDictionary Empty = new(); +public class DataDictionary : Dictionary +{ + public static readonly DataDictionary Empty = new(); - public DataDictionary() : base(StringComparer.OrdinalIgnoreCase) { } + public DataDictionary() : base(StringComparer.OrdinalIgnoreCase) { } - public DataDictionary(IEnumerable> values) : base(StringComparer.OrdinalIgnoreCase) + public DataDictionary(IEnumerable> values) : base(StringComparer.OrdinalIgnoreCase) + { + if (values != null) { - if (values != null) - { - foreach (var kvp in values) - Add(kvp.Key, kvp.Value); - } + foreach (var kvp in values) + Add(kvp.Key, kvp.Value); } } +} - public static class DataDictionaryExtensions +public static class DataDictionaryExtensions +{ + public static object GetValueOrDefault(this IDictionary dictionary, string key) { - public static object GetValueOrDefault(this IDictionary dictionary, string key) - { - return dictionary.TryGetValue(key, out object value) ? value : null; - } - - public static object GetValueOrDefault(this IDictionary dictionary, string key, object defaultValue) - { - return dictionary.TryGetValue(key, out object value) ? value : defaultValue; - } - - public static object GetValueOrDefault(this IDictionary dictionary, string key, Func defaultValueProvider) - { - return dictionary.TryGetValue(key, out object value) ? value : defaultValueProvider(); - } + return dictionary.TryGetValue(key, out object value) ? value : null; + } - public static T GetValue(this IDictionary dictionary, string key) - { - if (!dictionary.ContainsKey(key)) - throw new KeyNotFoundException($"Key \"{key}\" not found in the dictionary"); + public static object GetValueOrDefault(this IDictionary dictionary, string key, object defaultValue) + { + return dictionary.TryGetValue(key, out object value) ? value : defaultValue; + } - return dictionary.GetValueOrDefault(key); - } + public static object GetValueOrDefault(this IDictionary dictionary, string key, Func defaultValueProvider) + { + return dictionary.TryGetValue(key, out object value) ? value : defaultValueProvider(); + } - public static T GetValueOrDefault(this IDictionary dictionary, string key, T defaultValue = default) - { - if (!dictionary.ContainsKey(key)) - return defaultValue; + public static T GetValue(this IDictionary dictionary, string key) + { + if (!dictionary.ContainsKey(key)) + throw new KeyNotFoundException($"Key \"{key}\" not found in the dictionary"); - object data = dictionary[key]; - if (data is T t) - return t; + return dictionary.GetValueOrDefault(key); + } - if (data == null) - return defaultValue; + public static T GetValueOrDefault(this IDictionary dictionary, string key, T defaultValue = default) + { + if (!dictionary.ContainsKey(key)) + return defaultValue; - try - { - return data.ToType(); - } - catch { } + object data = dictionary[key]; + if (data is T t) + return t; + if (data == null) return defaultValue; - } - public static string GetString(this IDictionary dictionary, string name) + try { - return dictionary.GetString(name, String.Empty); + return data.ToType(); } + catch { } - public static string GetString(this IDictionary dictionary, string name, string @default) - { - if (!dictionary.TryGetValue(name, out object value)) - return @default; + return defaultValue; + } - if (value is string s) - return s; + public static string GetString(this IDictionary dictionary, string name) + { + return dictionary.GetString(name, String.Empty); + } - return String.Empty; - } + public static string GetString(this IDictionary dictionary, string name, string @default) + { + if (!dictionary.TryGetValue(name, out object value)) + return @default; + + if (value is string s) + return s; + + return String.Empty; } +} - public static class HaveDataExtensions +public static class HaveDataExtensions +{ + /// + /// Will get a value from the data dictionary and attempt to convert it to the target type using various type conversions + /// as well as using either the passed in or one accessed from the accessor. + /// + /// The data type to be returned + /// The source containing the data + /// They data dictionary key + /// The default value to return if the value doesn't exist + /// The serializer to use to convert the type from or array + /// The value from the data dictionary converted to the desired type + public static T GetDataOrDefault(this IHaveData target, string key, T defaultValue = default, ISerializer serializer = null) { - /// - /// Will get a value from the data dictionary and attempt to convert it to the target type using various type conversions - /// as well as using either the passed in or one accessed from the accessor. - /// - /// The data type to be returned - /// The source containing the data - /// They data dictionary key - /// The default value to return if the value doesn't exist - /// The serializer to use to convert the type from or array - /// The value from the data dictionary converted to the desired type - public static T GetDataOrDefault(this IHaveData target, string key, T defaultValue = default, ISerializer serializer = null) - { - if (serializer == null && target is IHaveSerializer haveSerializer) - serializer = haveSerializer.Serializer; + if (serializer == null && target is IHaveSerializer haveSerializer) + serializer = haveSerializer.Serializer; - if (target.Data.TryGetValue(key, out var value)) - return value.ToType(serializer); + if (target.Data.TryGetValue(key, out var value)) + return value.ToType(serializer); - return defaultValue; - } + return defaultValue; + } - /// - /// Will get a value from the data dictionary and attempt to convert it to the target type using various type conversions - /// as well as using either the passed in or one accessed from the accessor. - /// - /// The data type to be returned - /// The source containing the data - /// They data dictionary key - /// The value from the data dictionary converted to the desired type - /// The serializer to use to convert the type from or array - /// Whether or not we successfully got and converted the data - public static bool TryGetData(this IHaveData target, string key, out T value, ISerializer serializer = null) - { - if (serializer == null && target is IHaveSerializer haveSerializer) - serializer = haveSerializer.Serializer; + /// + /// Will get a value from the data dictionary and attempt to convert it to the target type using various type conversions + /// as well as using either the passed in or one accessed from the accessor. + /// + /// The data type to be returned + /// The source containing the data + /// They data dictionary key + /// The value from the data dictionary converted to the desired type + /// The serializer to use to convert the type from or array + /// Whether or not we successfully got and converted the data + public static bool TryGetData(this IHaveData target, string key, out T value, ISerializer serializer = null) + { + if (serializer == null && target is IHaveSerializer haveSerializer) + serializer = haveSerializer.Serializer; - value = default; + value = default; - if (!target.Data.TryGetValue(key, out object dataValue)) - return false; + if (!target.Data.TryGetValue(key, out object dataValue)) + return false; - value = dataValue.ToType(serializer); + value = dataValue.ToType(serializer); - return true; - } + return true; } } diff --git a/src/Foundatio/Utility/DisposableAction.cs b/src/Foundatio/Utility/DisposableAction.cs index 3ebaebebe..fe6f1bd43 100644 --- a/src/Foundatio/Utility/DisposableAction.cs +++ b/src/Foundatio/Utility/DisposableAction.cs @@ -1,34 +1,33 @@ using System; -namespace Foundatio.Utility +namespace Foundatio.Utility; + +/// +/// A class that will call an when Disposed. +/// +public sealed class DisposableAction : IDisposable { + private readonly Action _exitAction; + private bool _disposed; + /// - /// A class that will call an when Disposed. + /// Initializes a new instance of the class. /// - public sealed class DisposableAction : IDisposable + /// The exit action. + public DisposableAction(Action exitAction) { - private readonly Action _exitAction; - private bool _disposed; - - /// - /// Initializes a new instance of the class. - /// - /// The exit action. - public DisposableAction(Action exitAction) - { - _exitAction = exitAction; - } + _exitAction = exitAction; + } - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// - void IDisposable.Dispose() - { - if (_disposed) - return; + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + void IDisposable.Dispose() + { + if (_disposed) + return; - _exitAction(); - _disposed = true; - } + _exitAction(); + _disposed = true; } } diff --git a/src/Foundatio/Utility/EmptyDisposable.cs b/src/Foundatio/Utility/EmptyDisposable.cs index 727a19755..71c2202a7 100644 --- a/src/Foundatio/Utility/EmptyDisposable.cs +++ b/src/Foundatio/Utility/EmptyDisposable.cs @@ -2,44 +2,43 @@ using System.Threading.Tasks; using Foundatio.Lock; -namespace Foundatio.Utility -{ - public class EmptyDisposable : IDisposable - { - public void Dispose() { } - } +namespace Foundatio.Utility; - public class EmptyLock : ILock - { - public string LockId => String.Empty; +public class EmptyDisposable : IDisposable +{ + public void Dispose() { } +} - public string Resource => String.Empty; +public class EmptyLock : ILock +{ + public string LockId => String.Empty; - public DateTime AcquiredTimeUtc => DateTime.MinValue; + public string Resource => String.Empty; - public TimeSpan TimeWaitedForLock => TimeSpan.Zero; + public DateTime AcquiredTimeUtc => DateTime.MinValue; - public int RenewalCount => 0; + public TimeSpan TimeWaitedForLock => TimeSpan.Zero; - public ValueTask DisposeAsync() - { - return new ValueTask(); - } + public int RenewalCount => 0; - public Task RenewAsync(TimeSpan? lockExtension = null) - { - return Task.CompletedTask; - } + public ValueTask DisposeAsync() + { + return new ValueTask(); + } - public Task ReleaseAsync() - { - return Task.CompletedTask; - } + public Task RenewAsync(TimeSpan? lockExtension = null) + { + return Task.CompletedTask; } - public static class Disposable + public Task ReleaseAsync() { - public static IDisposable Empty = new EmptyDisposable(); - public static ILock EmptyLock = new EmptyLock(); + return Task.CompletedTask; } } + +public static class Disposable +{ + public static IDisposable Empty = new EmptyDisposable(); + public static ILock EmptyLock = new EmptyLock(); +} diff --git a/src/Foundatio/Utility/IAsyncDisposable.cs b/src/Foundatio/Utility/IAsyncDisposable.cs index f5971b300..fc4094300 100644 --- a/src/Foundatio/Utility/IAsyncDisposable.cs +++ b/src/Foundatio/Utility/IAsyncDisposable.cs @@ -2,60 +2,59 @@ using System.Runtime.ExceptionServices; using System.Threading.Tasks; -namespace Foundatio.Utility +namespace Foundatio.Utility; + +public static class Async { - public static class Async + public static async Task Using(TResource resource, Func> body) + where TResource : IAsyncDisposable { - public static async Task Using(TResource resource, Func> body) - where TResource : IAsyncDisposable + Exception exception = null; + var result = default(TReturn); + try { - Exception exception = null; - var result = default(TReturn); - try - { - result = await body(resource).AnyContext(); - } - catch (Exception ex) - { - exception = ex; - } - - await resource.DisposeAsync().AnyContext(); - if (exception != null) - { - var info = ExceptionDispatchInfo.Capture(exception); - info.Throw(); - } - - return result; + result = await body(resource).AnyContext(); } - - public static Task Using(TResource resource, Func body) where TResource : IAsyncDisposable + catch (Exception ex) { - return Using(resource, r => body()); + exception = ex; } - public static Task Using(TResource resource, Action body) where TResource : IAsyncDisposable + await resource.DisposeAsync().AnyContext(); + if (exception != null) { - return Using(resource, r => - { - body(); - return Task.CompletedTask; - }); + var info = ExceptionDispatchInfo.Capture(exception); + info.Throw(); } - public static Task Using(TResource resource, Func body) where TResource : IAsyncDisposable + return result; + } + + public static Task Using(TResource resource, Func body) where TResource : IAsyncDisposable + { + return Using(resource, r => body()); + } + + public static Task Using(TResource resource, Action body) where TResource : IAsyncDisposable + { + return Using(resource, r => { - return Using(resource, async r => - { - await body(resource).AnyContext(); - return Task.CompletedTask; - }); - } + body(); + return Task.CompletedTask; + }); + } - public static Task Using(TResource resource, Func> body) where TResource : IAsyncDisposable + public static Task Using(TResource resource, Func body) where TResource : IAsyncDisposable + { + return Using(resource, async r => { - return Using(resource, r => body()); - } + await body(resource).AnyContext(); + return Task.CompletedTask; + }); + } + + public static Task Using(TResource resource, Func> body) where TResource : IAsyncDisposable + { + return Using(resource, r => body()); } } diff --git a/src/Foundatio/Utility/IAsyncLifetime.cs b/src/Foundatio/Utility/IAsyncLifetime.cs index e583b99f3..0cef7348b 100644 --- a/src/Foundatio/Utility/IAsyncLifetime.cs +++ b/src/Foundatio/Utility/IAsyncLifetime.cs @@ -2,10 +2,9 @@ using System.Runtime.ExceptionServices; using System.Threading.Tasks; -namespace Foundatio.Utility +namespace Foundatio.Utility; + +public interface IAsyncLifetime : IAsyncDisposable { - public interface IAsyncLifetime : IAsyncDisposable - { - Task InitializeAsync(); - } + Task InitializeAsync(); } diff --git a/src/Foundatio/Utility/IHaveLogger.cs b/src/Foundatio/Utility/IHaveLogger.cs index 4ca17e0fc..280839630 100644 --- a/src/Foundatio/Utility/IHaveLogger.cs +++ b/src/Foundatio/Utility/IHaveLogger.cs @@ -1,18 +1,17 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -namespace Foundatio.Utility +namespace Foundatio.Utility; + +public interface IHaveLogger { - public interface IHaveLogger - { - ILogger Logger { get; } - } + ILogger Logger { get; } +} - public static class LoggerExtensions +public static class LoggerExtensions +{ + public static ILogger GetLogger(this object target) { - public static ILogger GetLogger(this object target) - { - return target is IHaveLogger accessor ? accessor.Logger ?? NullLogger.Instance : NullLogger.Instance; - } + return target is IHaveLogger accessor ? accessor.Logger ?? NullLogger.Instance : NullLogger.Instance; } } diff --git a/src/Foundatio/Utility/MaintenanceBase.cs b/src/Foundatio/Utility/MaintenanceBase.cs index 520c2e39a..7d5c9efdd 100644 --- a/src/Foundatio/Utility/MaintenanceBase.cs +++ b/src/Foundatio/Utility/MaintenanceBase.cs @@ -3,38 +3,37 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -namespace Foundatio.Utility +namespace Foundatio.Utility; + +public class MaintenanceBase : IDisposable { - public class MaintenanceBase : IDisposable - { - private ScheduledTimer _maintenanceTimer; - private readonly ILoggerFactory _loggerFactory; - protected readonly ILogger _logger; + private ScheduledTimer _maintenanceTimer; + private readonly ILoggerFactory _loggerFactory; + protected readonly ILogger _logger; - public MaintenanceBase(ILoggerFactory loggerFactory) - { - _loggerFactory = loggerFactory ?? NullLoggerFactory.Instance; - _logger = _loggerFactory.CreateLogger(GetType()); - } + public MaintenanceBase(ILoggerFactory loggerFactory) + { + _loggerFactory = loggerFactory ?? NullLoggerFactory.Instance; + _logger = _loggerFactory.CreateLogger(GetType()); + } - protected void InitializeMaintenance(TimeSpan? dueTime = null, TimeSpan? intervalTime = null) - { - _maintenanceTimer = new ScheduledTimer(DoMaintenanceAsync, dueTime, intervalTime, _loggerFactory); - } + protected void InitializeMaintenance(TimeSpan? dueTime = null, TimeSpan? intervalTime = null) + { + _maintenanceTimer = new ScheduledTimer(DoMaintenanceAsync, dueTime, intervalTime, _loggerFactory); + } - protected void ScheduleNextMaintenance(DateTime utcDate) - { - _maintenanceTimer.ScheduleNext(utcDate); - } + protected void ScheduleNextMaintenance(DateTime utcDate) + { + _maintenanceTimer.ScheduleNext(utcDate); + } - protected virtual Task DoMaintenanceAsync() - { - return Task.FromResult(DateTime.MaxValue); - } + protected virtual Task DoMaintenanceAsync() + { + return Task.FromResult(DateTime.MaxValue); + } - public virtual void Dispose() - { - _maintenanceTimer?.Dispose(); - } + public virtual void Dispose() + { + _maintenanceTimer?.Dispose(); } } diff --git a/src/Foundatio/Utility/OptionsBuilder.cs b/src/Foundatio/Utility/OptionsBuilder.cs index 20cae9fe5..d838d8705 100644 --- a/src/Foundatio/Utility/OptionsBuilder.cs +++ b/src/Foundatio/Utility/OptionsBuilder.cs @@ -1,35 +1,34 @@ using System; -namespace Foundatio +namespace Foundatio; + +public interface IOptionsBuilder { - public interface IOptionsBuilder - { - object Target { get; } - } + object Target { get; } +} - public interface IOptionsBuilder : IOptionsBuilder - { - T Build(); - } +public interface IOptionsBuilder : IOptionsBuilder +{ + T Build(); +} - public static class OptionsBuilderExtensions +public static class OptionsBuilderExtensions +{ + public static T Target(this IOptionsBuilder builder) { - public static T Target(this IOptionsBuilder builder) - { - return (T)builder.Target; - } + return (T)builder.Target; } +} - public class OptionsBuilder : IOptionsBuilder where T : class, new() - { - public T Target { get; } = new T(); - object IOptionsBuilder.Target => Target; +public class OptionsBuilder : IOptionsBuilder where T : class, new() +{ + public T Target { get; } = new T(); + object IOptionsBuilder.Target => Target; - public virtual T Build() - { - return Target; - } + public virtual T Build() + { + return Target; } - - public delegate TBuilder Builder(TBuilder builder) where TBuilder : class, IOptionsBuilder, new(); } + +public delegate TBuilder Builder(TBuilder builder) where TBuilder : class, IOptionsBuilder, new(); diff --git a/src/Foundatio/Utility/PathHelper.cs b/src/Foundatio/Utility/PathHelper.cs index 479fa5fd7..84c304cbf 100644 --- a/src/Foundatio/Utility/PathHelper.cs +++ b/src/Foundatio/Utility/PathHelper.cs @@ -1,57 +1,56 @@ using System; using System.IO; -namespace Foundatio.Utility +namespace Foundatio.Utility; + +public static class PathHelper { - public static class PathHelper - { - private const string DATA_DIRECTORY = "|DataDirectory|"; + private const string DATA_DIRECTORY = "|DataDirectory|"; - public static string ExpandPath(string path) - { - if (String.IsNullOrEmpty(path)) - return path; + public static string ExpandPath(string path) + { + if (String.IsNullOrEmpty(path)) + return path; - path = path.Replace('/', Path.DirectorySeparatorChar); - path = path.Replace('\\', Path.DirectorySeparatorChar); + path = path.Replace('/', Path.DirectorySeparatorChar); + path = path.Replace('\\', Path.DirectorySeparatorChar); - if (!path.StartsWith(DATA_DIRECTORY, StringComparison.OrdinalIgnoreCase)) - return Path.GetFullPath(path); + if (!path.StartsWith(DATA_DIRECTORY, StringComparison.OrdinalIgnoreCase)) + return Path.GetFullPath(path); - string dataDirectory = GetDataDirectory(); - int length = DATA_DIRECTORY.Length; - if (path.Length <= length) - return dataDirectory; + string dataDirectory = GetDataDirectory(); + int length = DATA_DIRECTORY.Length; + if (path.Length <= length) + return dataDirectory; - string relativePath = path.Substring(length); - char c = relativePath[0]; + string relativePath = path.Substring(length); + char c = relativePath[0]; - if (c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar) - relativePath = relativePath.Substring(1); + if (c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar) + relativePath = relativePath.Substring(1); - string fullPath = Path.Combine(dataDirectory ?? String.Empty, relativePath); - fullPath = Path.GetFullPath(fullPath); + string fullPath = Path.Combine(dataDirectory ?? String.Empty, relativePath); + fullPath = Path.GetFullPath(fullPath); - return fullPath; - } + return fullPath; + } - public static string GetDataDirectory() + public static string GetDataDirectory() + { + try { - try - { - string dataDirectory = AppDomain.CurrentDomain.GetData("DataDirectory") as string; - if (String.IsNullOrEmpty(dataDirectory)) - dataDirectory = AppContext.BaseDirectory; - - if (!String.IsNullOrEmpty(dataDirectory)) - return Path.GetFullPath(dataDirectory); - } - catch (Exception) - { - return null; - } + string dataDirectory = AppDomain.CurrentDomain.GetData("DataDirectory") as string; + if (String.IsNullOrEmpty(dataDirectory)) + dataDirectory = AppContext.BaseDirectory; + if (!String.IsNullOrEmpty(dataDirectory)) + return Path.GetFullPath(dataDirectory); + } + catch (Exception) + { return null; } + + return null; } } diff --git a/src/Foundatio/Utility/Run.cs b/src/Foundatio/Utility/Run.cs index 31806b19f..9283bbfd9 100644 --- a/src/Foundatio/Utility/Run.cs +++ b/src/Foundatio/Utility/Run.cs @@ -4,80 +4,79 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; -namespace Foundatio.Utility +namespace Foundatio.Utility; + +public static class Run { - public static class Run + public static Task DelayedAsync(TimeSpan delay, Func action, CancellationToken cancellationToken = default) { - public static Task DelayedAsync(TimeSpan delay, Func action, CancellationToken cancellationToken = default) + if (cancellationToken.IsCancellationRequested) + return Task.CompletedTask; + + return Task.Run(async () => { - if (cancellationToken.IsCancellationRequested) - return Task.CompletedTask; + if (delay.Ticks > 0) + await SystemClock.SleepAsync(delay, cancellationToken).AnyContext(); - return Task.Run(async () => - { - if (delay.Ticks > 0) - await SystemClock.SleepAsync(delay, cancellationToken).AnyContext(); + if (cancellationToken.IsCancellationRequested) + return; - if (cancellationToken.IsCancellationRequested) - return; + await action().AnyContext(); + }, cancellationToken); + } - await action().AnyContext(); - }, cancellationToken); - } + public static Task InParallelAsync(int iterations, Func work) + { + return Task.WhenAll(Enumerable.Range(1, iterations).Select(i => Task.Run(() => work(i)))); + } - public static Task InParallelAsync(int iterations, Func work) + public static Task WithRetriesAsync(Func action, int maxAttempts = 5, TimeSpan? retryInterval = null, CancellationToken cancellationToken = default, ILogger logger = null) + { + return WithRetriesAsync(async () => { - return Task.WhenAll(Enumerable.Range(1, iterations).Select(i => Task.Run(() => work(i)))); - } + await action().AnyContext(); + return null; + }, maxAttempts, retryInterval, cancellationToken, logger); + } - public static Task WithRetriesAsync(Func action, int maxAttempts = 5, TimeSpan? retryInterval = null, CancellationToken cancellationToken = default, ILogger logger = null) - { - return WithRetriesAsync(async () => - { - await action().AnyContext(); - return null; - }, maxAttempts, retryInterval, cancellationToken, logger); - } + public static async Task WithRetriesAsync(Func> action, int maxAttempts = 5, TimeSpan? retryInterval = null, CancellationToken cancellationToken = default, ILogger logger = null) + { + if (action == null) + throw new ArgumentNullException(nameof(action)); - public static async Task WithRetriesAsync(Func> action, int maxAttempts = 5, TimeSpan? retryInterval = null, CancellationToken cancellationToken = default, ILogger logger = null) - { - if (action == null) - throw new ArgumentNullException(nameof(action)); + int attempts = 1; + var startTime = SystemClock.UtcNow; + int currentBackoffTime = _defaultBackoffIntervals[0]; + if (retryInterval != null) + currentBackoffTime = (int)retryInterval.Value.TotalMilliseconds; - int attempts = 1; - var startTime = SystemClock.UtcNow; - int currentBackoffTime = _defaultBackoffIntervals[0]; - if (retryInterval != null) - currentBackoffTime = (int)retryInterval.Value.TotalMilliseconds; + do + { + if (attempts > 1 && logger != null && logger.IsEnabled(LogLevel.Information)) + logger.LogInformation("Retrying {Attempts} attempt after {Delay:g}...", attempts.ToOrdinal(), SystemClock.UtcNow.Subtract(startTime)); - do + try { - if (attempts > 1 && logger != null && logger.IsEnabled(LogLevel.Information)) - logger.LogInformation("Retrying {Attempts} attempt after {Delay:g}...", attempts.ToOrdinal(), SystemClock.UtcNow.Subtract(startTime)); - - try - { - return await action().AnyContext(); - } - catch (Exception ex) - { - if (attempts >= maxAttempts) - throw; - - if (logger != null && logger.IsEnabled(LogLevel.Error)) - logger.LogError(ex, "Retry error: {Message}", ex.Message); + return await action().AnyContext(); + } + catch (Exception ex) + { + if (attempts >= maxAttempts) + throw; - await SystemClock.SleepSafeAsync(currentBackoffTime, cancellationToken).AnyContext(); - } + if (logger != null && logger.IsEnabled(LogLevel.Error)) + logger.LogError(ex, "Retry error: {Message}", ex.Message); - if (retryInterval == null) - currentBackoffTime = _defaultBackoffIntervals[Math.Min(attempts, _defaultBackoffIntervals.Length - 1)]; - attempts++; - } while (attempts <= maxAttempts && !cancellationToken.IsCancellationRequested); + await SystemClock.SleepSafeAsync(currentBackoffTime, cancellationToken).AnyContext(); + } - throw new TaskCanceledException("Should not get here"); - } + if (retryInterval == null) + currentBackoffTime = _defaultBackoffIntervals[Math.Min(attempts, _defaultBackoffIntervals.Length - 1)]; + attempts++; + } while (attempts <= maxAttempts && !cancellationToken.IsCancellationRequested); - private static readonly int[] _defaultBackoffIntervals = new int[] { 100, 1000, 2000, 2000, 5000, 5000, 10000, 30000, 60000 }; + throw new TaskCanceledException("Should not get here"); } + + private static readonly int[] _defaultBackoffIntervals = new int[] { 100, 1000, 2000, 2000, 5000, 5000, 10000, 30000, 60000 }; } diff --git a/src/Foundatio/Utility/ScheduledTimer.cs b/src/Foundatio/Utility/ScheduledTimer.cs index 5308e928f..045e2daa2 100644 --- a/src/Foundatio/Utility/ScheduledTimer.cs +++ b/src/Foundatio/Utility/ScheduledTimer.cs @@ -6,44 +6,61 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -namespace Foundatio.Utility +namespace Foundatio.Utility; + +public class ScheduledTimer : IDisposable { - public class ScheduledTimer : IDisposable + private DateTime _next = DateTime.MaxValue; + private DateTime _last = DateTime.MinValue; + private readonly Timer _timer; + private readonly ILogger _logger; + private readonly Func> _timerCallback; + private readonly TimeSpan _minimumInterval; + private readonly AsyncLock _lock = new(); + private bool _isRunning = false; + private bool _shouldRunAgainImmediately = false; + + public ScheduledTimer(Func> timerCallback, TimeSpan? dueTime = null, TimeSpan? minimumIntervalTime = null, ILoggerFactory loggerFactory = null) { - private DateTime _next = DateTime.MaxValue; - private DateTime _last = DateTime.MinValue; - private readonly Timer _timer; - private readonly ILogger _logger; - private readonly Func> _timerCallback; - private readonly TimeSpan _minimumInterval; - private readonly AsyncLock _lock = new(); - private bool _isRunning = false; - private bool _shouldRunAgainImmediately = false; - - public ScheduledTimer(Func> timerCallback, TimeSpan? dueTime = null, TimeSpan? minimumIntervalTime = null, ILoggerFactory loggerFactory = null) - { - _logger = loggerFactory?.CreateLogger() ?? NullLogger.Instance; - _timerCallback = timerCallback ?? throw new ArgumentNullException(nameof(timerCallback)); - _minimumInterval = minimumIntervalTime ?? TimeSpan.Zero; + _logger = loggerFactory?.CreateLogger() ?? NullLogger.Instance; + _timerCallback = timerCallback ?? throw new ArgumentNullException(nameof(timerCallback)); + _minimumInterval = minimumIntervalTime ?? TimeSpan.Zero; + + int dueTimeMs = dueTime.HasValue ? (int)dueTime.Value.TotalMilliseconds : Timeout.Infinite; + _timer = new Timer(s => Task.Run(RunCallbackAsync), null, dueTimeMs, Timeout.Infinite); + } + + public void ScheduleNext(DateTime? utcDate = null) + { + var utcNow = SystemClock.UtcNow; + if (!utcDate.HasValue || utcDate.Value < utcNow) + utcDate = utcNow; - int dueTimeMs = dueTime.HasValue ? (int)dueTime.Value.TotalMilliseconds : Timeout.Infinite; - _timer = new Timer(s => Task.Run(RunCallbackAsync), null, dueTimeMs, Timeout.Infinite); + bool isTraceLogLevelEnabled = _logger.IsEnabled(LogLevel.Trace); + if (isTraceLogLevelEnabled) _logger.LogTrace("ScheduleNext called: value={NextRun:O}", utcDate.Value); + if (utcDate == DateTime.MaxValue) + { + if (isTraceLogLevelEnabled) _logger.LogTrace("Ignoring MaxValue"); + return; } - public void ScheduleNext(DateTime? utcDate = null) + // already have an earlier scheduled time + if (_next > utcNow && utcDate > _next) { - var utcNow = SystemClock.UtcNow; - if (!utcDate.HasValue || utcDate.Value < utcNow) - utcDate = utcNow; + if (isTraceLogLevelEnabled) + _logger.LogTrace("Ignoring because already scheduled for earlier time: {PreviousTicks} Next: {NextTicks}", utcDate.Value.Ticks, _next.Ticks); + return; + } - bool isTraceLogLevelEnabled = _logger.IsEnabled(LogLevel.Trace); - if (isTraceLogLevelEnabled) _logger.LogTrace("ScheduleNext called: value={NextRun:O}", utcDate.Value); - if (utcDate == DateTime.MaxValue) - { - if (isTraceLogLevelEnabled) _logger.LogTrace("Ignoring MaxValue"); - return; - } + // ignore duplicate times + if (_next == utcDate) + { + if (isTraceLogLevelEnabled) _logger.LogTrace("Ignoring because already scheduled for same time"); + return; + } + using (_lock.Lock()) + { // already have an earlier scheduled time if (_next > utcNow && utcDate > _next) { @@ -59,39 +76,34 @@ public void ScheduleNext(DateTime? utcDate = null) return; } - using (_lock.Lock()) - { - // already have an earlier scheduled time - if (_next > utcNow && utcDate > _next) - { - if (isTraceLogLevelEnabled) - _logger.LogTrace("Ignoring because already scheduled for earlier time: {PreviousTicks} Next: {NextTicks}", utcDate.Value.Ticks, _next.Ticks); - return; - } - - // ignore duplicate times - if (_next == utcDate) - { - if (isTraceLogLevelEnabled) _logger.LogTrace("Ignoring because already scheduled for same time"); - return; - } - - int delay = Math.Max((int)Math.Ceiling(utcDate.Value.Subtract(utcNow).TotalMilliseconds), 0); - _next = utcDate.Value; - if (_last == DateTime.MinValue) - _last = _next; - - if (isTraceLogLevelEnabled) _logger.LogTrace("Scheduling next: delay={Delay}", delay); - if (delay > 0) - _timer.Change(delay, Timeout.Infinite); - else - _ = Task.Run(RunCallbackAsync); - } + int delay = Math.Max((int)Math.Ceiling(utcDate.Value.Subtract(utcNow).TotalMilliseconds), 0); + _next = utcDate.Value; + if (_last == DateTime.MinValue) + _last = _next; + + if (isTraceLogLevelEnabled) _logger.LogTrace("Scheduling next: delay={Delay}", delay); + if (delay > 0) + _timer.Change(delay, Timeout.Infinite); + else + _ = Task.Run(RunCallbackAsync); + } + } + + private async Task RunCallbackAsync() + { + bool isTraceLogLevelEnabled = _logger.IsEnabled(LogLevel.Trace); + if (_isRunning) + { + if (isTraceLogLevelEnabled) + _logger.LogTrace("Exiting run callback because its already running, will run again immediately"); + + _shouldRunAgainImmediately = true; + return; } - private async Task RunCallbackAsync() + if (isTraceLogLevelEnabled) _logger.LogTrace("Starting RunCallbackAsync"); + using (await _lock.LockAsync().AnyContext()) { - bool isTraceLogLevelEnabled = _logger.IsEnabled(LogLevel.Trace); if (_isRunning) { if (isTraceLogLevelEnabled) @@ -101,74 +113,61 @@ private async Task RunCallbackAsync() return; } - if (isTraceLogLevelEnabled) _logger.LogTrace("Starting RunCallbackAsync"); - using (await _lock.LockAsync().AnyContext()) - { - if (_isRunning) - { - if (isTraceLogLevelEnabled) - _logger.LogTrace("Exiting run callback because its already running, will run again immediately"); - - _shouldRunAgainImmediately = true; - return; - } + _last = SystemClock.UtcNow; + } - _last = SystemClock.UtcNow; - } + try + { + _isRunning = true; + DateTime? next = null; + var sw = Stopwatch.StartNew(); try { - _isRunning = true; - DateTime? next = null; - - var sw = Stopwatch.StartNew(); - try - { - next = await _timerCallback().AnyContext(); - } - catch (Exception ex) - { - if (_logger.IsEnabled(LogLevel.Error)) - _logger.LogError(ex, "Error running scheduled timer callback: {Message}", ex.Message); - - _shouldRunAgainImmediately = true; - } - finally - { - sw.Stop(); - if (isTraceLogLevelEnabled) _logger.LogTrace("Callback took: {Elapsed:g}", sw.Elapsed); - } - - if (_minimumInterval > TimeSpan.Zero) - { - if (isTraceLogLevelEnabled) _logger.LogTrace("Sleeping for minimum interval: {Interval:g}", _minimumInterval); - await SystemClock.SleepAsync(_minimumInterval).AnyContext(); - if (isTraceLogLevelEnabled) _logger.LogTrace("Finished sleeping"); - } - - var nextRun = SystemClock.UtcNow.AddMilliseconds(10); - if (_shouldRunAgainImmediately || next.HasValue && next.Value <= nextRun) - ScheduleNext(nextRun); - else if (next.HasValue) - ScheduleNext(next.Value); + next = await _timerCallback().AnyContext(); } catch (Exception ex) { if (_logger.IsEnabled(LogLevel.Error)) - _logger.LogError(ex, "Error running schedule next callback: {Message}", ex.Message); + _logger.LogError(ex, "Error running scheduled timer callback: {Message}", ex.Message); + + _shouldRunAgainImmediately = true; } finally { - _isRunning = false; - _shouldRunAgainImmediately = false; + sw.Stop(); + if (isTraceLogLevelEnabled) _logger.LogTrace("Callback took: {Elapsed:g}", sw.Elapsed); } - if (isTraceLogLevelEnabled) _logger.LogTrace("Finished RunCallbackAsync"); - } + if (_minimumInterval > TimeSpan.Zero) + { + if (isTraceLogLevelEnabled) _logger.LogTrace("Sleeping for minimum interval: {Interval:g}", _minimumInterval); + await SystemClock.SleepAsync(_minimumInterval).AnyContext(); + if (isTraceLogLevelEnabled) _logger.LogTrace("Finished sleeping"); + } - public void Dispose() + var nextRun = SystemClock.UtcNow.AddMilliseconds(10); + if (_shouldRunAgainImmediately || next.HasValue && next.Value <= nextRun) + ScheduleNext(nextRun); + else if (next.HasValue) + ScheduleNext(next.Value); + } + catch (Exception ex) + { + if (_logger.IsEnabled(LogLevel.Error)) + _logger.LogError(ex, "Error running schedule next callback: {Message}", ex.Message); + } + finally { - _timer?.Dispose(); + _isRunning = false; + _shouldRunAgainImmediately = false; } + + if (isTraceLogLevelEnabled) _logger.LogTrace("Finished RunCallbackAsync"); + } + + public void Dispose() + { + _timer?.Dispose(); } } diff --git a/src/Foundatio/Utility/SharedOptions.cs b/src/Foundatio/Utility/SharedOptions.cs index f51b4d6af..7df27a460 100644 --- a/src/Foundatio/Utility/SharedOptions.cs +++ b/src/Foundatio/Utility/SharedOptions.cs @@ -2,28 +2,27 @@ using Foundatio.Serializer; using Microsoft.Extensions.Logging; -namespace Foundatio +namespace Foundatio; + +public class SharedOptions { - public class SharedOptions + public ISerializer Serializer { get; set; } + public ILoggerFactory LoggerFactory { get; set; } +} + +public class SharedOptionsBuilder : OptionsBuilder + where TOption : SharedOptions, new() + where TBuilder : SharedOptionsBuilder +{ + public TBuilder Serializer(ISerializer serializer) { - public ISerializer Serializer { get; set; } - public ILoggerFactory LoggerFactory { get; set; } + Target.Serializer = serializer; + return (TBuilder)this; } - public class SharedOptionsBuilder : OptionsBuilder - where TOption : SharedOptions, new() - where TBuilder : SharedOptionsBuilder + public TBuilder LoggerFactory(ILoggerFactory loggerFactory) { - public TBuilder Serializer(ISerializer serializer) - { - Target.Serializer = serializer; - return (TBuilder)this; - } - - public TBuilder LoggerFactory(ILoggerFactory loggerFactory) - { - Target.LoggerFactory = loggerFactory; - return (TBuilder)this; - } + Target.LoggerFactory = loggerFactory; + return (TBuilder)this; } } diff --git a/src/Foundatio/Utility/SystemClock.cs b/src/Foundatio/Utility/SystemClock.cs index 844a4b07d..2cb2fad90 100644 --- a/src/Foundatio/Utility/SystemClock.cs +++ b/src/Foundatio/Utility/SystemClock.cs @@ -2,235 +2,234 @@ using System.Threading; using System.Threading.Tasks; -namespace Foundatio.Utility +namespace Foundatio.Utility; + +public interface ISystemClock { - public interface ISystemClock - { - DateTime Now(); - DateTime UtcNow(); - DateTimeOffset OffsetNow(); - DateTimeOffset OffsetUtcNow(); - void Sleep(int milliseconds); - Task SleepAsync(int milliseconds, CancellationToken ct); - TimeSpan TimeZoneOffset(); - } + DateTime Now(); + DateTime UtcNow(); + DateTimeOffset OffsetNow(); + DateTimeOffset OffsetUtcNow(); + void Sleep(int milliseconds); + Task SleepAsync(int milliseconds, CancellationToken ct); + TimeSpan TimeZoneOffset(); +} + +public class RealSystemClock : ISystemClock +{ + public static readonly RealSystemClock Instance = new(); + + public DateTime Now() => DateTime.Now; + public DateTime UtcNow() => DateTime.UtcNow; + public DateTimeOffset OffsetNow() => DateTimeOffset.Now; + public DateTimeOffset OffsetUtcNow() => DateTimeOffset.UtcNow; + public void Sleep(int milliseconds) => Thread.Sleep(milliseconds); + public Task SleepAsync(int milliseconds, CancellationToken ct) => Task.Delay(milliseconds, ct); + public TimeSpan TimeZoneOffset() => DateTimeOffset.Now.Offset; +} + +internal class TestSystemClockImpl : ISystemClock, IDisposable +{ + private DateTime? _fixedUtc = null; + private TimeSpan _offset = TimeSpan.Zero; + private TimeSpan _timeZoneOffset = DateTimeOffset.Now.Offset; + private bool _fakeSleep = false; + private ISystemClock _originalClock; - public class RealSystemClock : ISystemClock + public TestSystemClockImpl() { } + + public TestSystemClockImpl(ISystemClock originalTime) { - public static readonly RealSystemClock Instance = new(); - - public DateTime Now() => DateTime.Now; - public DateTime UtcNow() => DateTime.UtcNow; - public DateTimeOffset OffsetNow() => DateTimeOffset.Now; - public DateTimeOffset OffsetUtcNow() => DateTimeOffset.UtcNow; - public void Sleep(int milliseconds) => Thread.Sleep(milliseconds); - public Task SleepAsync(int milliseconds, CancellationToken ct) => Task.Delay(milliseconds, ct); - public TimeSpan TimeZoneOffset() => DateTimeOffset.Now.Offset; + _originalClock = originalTime; } - internal class TestSystemClockImpl : ISystemClock, IDisposable - { - private DateTime? _fixedUtc = null; - private TimeSpan _offset = TimeSpan.Zero; - private TimeSpan _timeZoneOffset = DateTimeOffset.Now.Offset; - private bool _fakeSleep = false; - private ISystemClock _originalClock; + public DateTime UtcNow() => (_fixedUtc ?? DateTime.UtcNow).Add(_offset); + public DateTime Now() => new(UtcNow().Ticks + TimeZoneOffset().Ticks, DateTimeKind.Local); + public DateTimeOffset OffsetNow() => new(UtcNow().Ticks + TimeZoneOffset().Ticks, TimeZoneOffset()); + public DateTimeOffset OffsetUtcNow() => new(UtcNow().Ticks, TimeSpan.Zero); + public TimeSpan TimeZoneOffset() => _timeZoneOffset; - public TestSystemClockImpl() { } + public void SetTimeZoneOffset(TimeSpan offset) => _timeZoneOffset = offset; + public void AddTime(TimeSpan amount) => _offset = _offset.Add(amount); + public void SubtractTime(TimeSpan amount) => _offset = _offset.Subtract(amount); + public void UseFakeSleep() => _fakeSleep = true; + public void UseRealSleep() => _fakeSleep = false; - public TestSystemClockImpl(ISystemClock originalTime) + public void Sleep(int milliseconds) + { + if (!_fakeSleep) { - _originalClock = originalTime; + Thread.Sleep(milliseconds); + return; } - public DateTime UtcNow() => (_fixedUtc ?? DateTime.UtcNow).Add(_offset); - public DateTime Now() => new(UtcNow().Ticks + TimeZoneOffset().Ticks, DateTimeKind.Local); - public DateTimeOffset OffsetNow() => new(UtcNow().Ticks + TimeZoneOffset().Ticks, TimeZoneOffset()); - public DateTimeOffset OffsetUtcNow() => new(UtcNow().Ticks, TimeSpan.Zero); - public TimeSpan TimeZoneOffset() => _timeZoneOffset; - - public void SetTimeZoneOffset(TimeSpan offset) => _timeZoneOffset = offset; - public void AddTime(TimeSpan amount) => _offset = _offset.Add(amount); - public void SubtractTime(TimeSpan amount) => _offset = _offset.Subtract(amount); - public void UseFakeSleep() => _fakeSleep = true; - public void UseRealSleep() => _fakeSleep = false; - - public void Sleep(int milliseconds) - { - if (!_fakeSleep) - { - Thread.Sleep(milliseconds); - return; - } + AddTime(TimeSpan.FromMilliseconds(milliseconds)); + Thread.Sleep(1); + } - AddTime(TimeSpan.FromMilliseconds(milliseconds)); - Thread.Sleep(1); - } + public Task SleepAsync(int milliseconds, CancellationToken ct) + { + if (!_fakeSleep) + return Task.Delay(milliseconds, ct); - public Task SleepAsync(int milliseconds, CancellationToken ct) - { - if (!_fakeSleep) - return Task.Delay(milliseconds, ct); + Sleep(milliseconds); + return Task.CompletedTask; + } - Sleep(milliseconds); - return Task.CompletedTask; - } + public void Freeze() + { + SetFrozenTime(Now()); + } - public void Freeze() - { - SetFrozenTime(Now()); - } + public void Unfreeze() + { + SetTime(Now()); + } - public void Unfreeze() - { - SetTime(Now()); - } + public void SetFrozenTime(DateTime time) + { + SetTime(time, true); + } - public void SetFrozenTime(DateTime time) + public void SetTime(DateTime time, bool freeze = false) + { + var now = DateTime.Now; + if (freeze) { - SetTime(time, true); - } + if (time.Kind == DateTimeKind.Unspecified) + time = time.ToUniversalTime(); - public void SetTime(DateTime time, bool freeze = false) - { - var now = DateTime.Now; - if (freeze) + if (time.Kind == DateTimeKind.Utc) { - if (time.Kind == DateTimeKind.Unspecified) - time = time.ToUniversalTime(); - - if (time.Kind == DateTimeKind.Utc) - { - _fixedUtc = time; - } - else if (time.Kind == DateTimeKind.Local) - { - _fixedUtc = new DateTime(time.Ticks - TimeZoneOffset().Ticks, DateTimeKind.Utc); - } + _fixedUtc = time; } - else + else if (time.Kind == DateTimeKind.Local) { - _fixedUtc = null; - - if (time.Kind == DateTimeKind.Unspecified) - time = time.ToUniversalTime(); - - if (time.Kind == DateTimeKind.Utc) - { - _offset = now.ToUniversalTime().Subtract(time); - } - else if (time.Kind == DateTimeKind.Local) - { - _offset = now.Subtract(time); - } + _fixedUtc = new DateTime(time.Ticks - TimeZoneOffset().Ticks, DateTimeKind.Utc); } } - - public void Dispose() + else { - if (_originalClock == null) - return; + _fixedUtc = null; - var originalClock = Interlocked.Exchange(ref _originalClock, null); - if (originalClock != null) - SystemClock.Instance = originalClock; - } + if (time.Kind == DateTimeKind.Unspecified) + time = time.ToUniversalTime(); - public static TestSystemClockImpl Instance - { - get + if (time.Kind == DateTimeKind.Utc) { - if (!(SystemClock.Instance is TestSystemClockImpl testClock)) - throw new ArgumentException("You must first install TestSystemClock using TestSystemClock.Install"); - - return testClock; + _offset = now.ToUniversalTime().Subtract(time); + } + else if (time.Kind == DateTimeKind.Local) + { + _offset = now.Subtract(time); } } } - public class TestSystemClock + public void Dispose() + { + if (_originalClock == null) + return; + + var originalClock = Interlocked.Exchange(ref _originalClock, null); + if (originalClock != null) + SystemClock.Instance = originalClock; + } + + public static TestSystemClockImpl Instance { - public static void SetTimeZoneOffset(TimeSpan offset) => TestSystemClockImpl.Instance.SetTimeZoneOffset(offset); - public static void AddTime(TimeSpan amount) => TestSystemClockImpl.Instance.AddTime(amount); - public static void SubtractTime(TimeSpan amount) => TestSystemClockImpl.Instance.SubtractTime(amount); - public static void UseFakeSleep() => TestSystemClockImpl.Instance.UseFakeSleep(); - public static void UseRealSleep() => TestSystemClockImpl.Instance.UseRealSleep(); - public static void Freeze() => TestSystemClockImpl.Instance.Freeze(); - public static void Unfreeze() => TestSystemClockImpl.Instance.Unfreeze(); - public static void SetFrozenTime(DateTime time) => TestSystemClockImpl.Instance.SetFrozenTime(time); - public static void SetTime(DateTime time, bool freeze = false) => TestSystemClockImpl.Instance.SetTime(time, freeze); - - public static IDisposable Install() + get { - var testClock = new TestSystemClockImpl(SystemClock.Instance); - SystemClock.Instance = testClock; + if (!(SystemClock.Instance is TestSystemClockImpl testClock)) + throw new ArgumentException("You must first install TestSystemClock using TestSystemClock.Install"); return testClock; } } +} - public static class SystemClock +public class TestSystemClock +{ + public static void SetTimeZoneOffset(TimeSpan offset) => TestSystemClockImpl.Instance.SetTimeZoneOffset(offset); + public static void AddTime(TimeSpan amount) => TestSystemClockImpl.Instance.AddTime(amount); + public static void SubtractTime(TimeSpan amount) => TestSystemClockImpl.Instance.SubtractTime(amount); + public static void UseFakeSleep() => TestSystemClockImpl.Instance.UseFakeSleep(); + public static void UseRealSleep() => TestSystemClockImpl.Instance.UseRealSleep(); + public static void Freeze() => TestSystemClockImpl.Instance.Freeze(); + public static void Unfreeze() => TestSystemClockImpl.Instance.Unfreeze(); + public static void SetFrozenTime(DateTime time) => TestSystemClockImpl.Instance.SetFrozenTime(time); + public static void SetTime(DateTime time, bool freeze = false) => TestSystemClockImpl.Instance.SetTime(time, freeze); + + public static IDisposable Install() { - private static AsyncLocal _instance; + var testClock = new TestSystemClockImpl(SystemClock.Instance); + SystemClock.Instance = testClock; + + return testClock; + } +} + +public static class SystemClock +{ + private static AsyncLocal _instance; - public static ISystemClock Instance + public static ISystemClock Instance + { + get => _instance?.Value ?? RealSystemClock.Instance; + set { - get => _instance?.Value ?? RealSystemClock.Instance; - set - { - if (_instance == null) - _instance = new AsyncLocal(); + if (_instance == null) + _instance = new AsyncLocal(); - _instance.Value = value; - } + _instance.Value = value; } + } - public static DateTime Now => Instance.Now(); - public static DateTime UtcNow => Instance.UtcNow(); - public static DateTimeOffset OffsetNow => Instance.OffsetNow(); - public static DateTimeOffset OffsetUtcNow => Instance.OffsetUtcNow(); - public static TimeSpan TimeZoneOffset => Instance.TimeZoneOffset(); - public static void Sleep(int milliseconds) => Instance.Sleep(milliseconds); - public static Task SleepAsync(int milliseconds, CancellationToken cancellationToken = default) - => Instance.SleepAsync(milliseconds, cancellationToken); + public static DateTime Now => Instance.Now(); + public static DateTime UtcNow => Instance.UtcNow(); + public static DateTimeOffset OffsetNow => Instance.OffsetNow(); + public static DateTimeOffset OffsetUtcNow => Instance.OffsetUtcNow(); + public static TimeSpan TimeZoneOffset => Instance.TimeZoneOffset(); + public static void Sleep(int milliseconds) => Instance.Sleep(milliseconds); + public static Task SleepAsync(int milliseconds, CancellationToken cancellationToken = default) + => Instance.SleepAsync(milliseconds, cancellationToken); - #region Extensions + #region Extensions - public static void Sleep(TimeSpan delay) - => Instance.Sleep(delay); + public static void Sleep(TimeSpan delay) + => Instance.Sleep(delay); - public static Task SleepAsync(TimeSpan delay, CancellationToken cancellationToken = default) - => Instance.SleepAsync(delay, cancellationToken); + public static Task SleepAsync(TimeSpan delay, CancellationToken cancellationToken = default) + => Instance.SleepAsync(delay, cancellationToken); - public static Task SleepSafeAsync(int milliseconds, CancellationToken cancellationToken = default) - { - return Instance.SleepSafeAsync(milliseconds, cancellationToken); - } + public static Task SleepSafeAsync(int milliseconds, CancellationToken cancellationToken = default) + { + return Instance.SleepSafeAsync(milliseconds, cancellationToken); + } - public static Task SleepSafeAsync(TimeSpan delay, CancellationToken cancellationToken = default) - => Instance.SleepSafeAsync(delay, cancellationToken); + public static Task SleepSafeAsync(TimeSpan delay, CancellationToken cancellationToken = default) + => Instance.SleepSafeAsync(delay, cancellationToken); - #endregion - } + #endregion +} - public static class TimeExtensions - { - public static void Sleep(this ISystemClock time, TimeSpan delay) - => time.Sleep((int)delay.TotalMilliseconds); +public static class TimeExtensions +{ + public static void Sleep(this ISystemClock time, TimeSpan delay) + => time.Sleep((int)delay.TotalMilliseconds); - public static Task SleepAsync(this ISystemClock time, TimeSpan delay, CancellationToken cancellationToken = default) - => time.SleepAsync((int)delay.TotalMilliseconds, cancellationToken); + public static Task SleepAsync(this ISystemClock time, TimeSpan delay, CancellationToken cancellationToken = default) + => time.SleepAsync((int)delay.TotalMilliseconds, cancellationToken); - public static async Task SleepSafeAsync(this ISystemClock time, int milliseconds, CancellationToken cancellationToken = default) + public static async Task SleepSafeAsync(this ISystemClock time, int milliseconds, CancellationToken cancellationToken = default) + { + try { - try - { - await time.SleepAsync(milliseconds, cancellationToken).AnyContext(); - } - catch (OperationCanceledException) { } + await time.SleepAsync(milliseconds, cancellationToken).AnyContext(); } - - public static Task SleepSafeAsync(this ISystemClock time, TimeSpan delay, CancellationToken cancellationToken = default) - => time.SleepSafeAsync((int)delay.TotalMilliseconds, cancellationToken); + catch (OperationCanceledException) { } } + + public static Task SleepSafeAsync(this ISystemClock time, TimeSpan delay, CancellationToken cancellationToken = default) + => time.SleepSafeAsync((int)delay.TotalMilliseconds, cancellationToken); } diff --git a/src/Foundatio/Utility/TimeUnit.cs b/src/Foundatio/Utility/TimeUnit.cs index a607bf2f9..1739db3e1 100644 --- a/src/Foundatio/Utility/TimeUnit.cs +++ b/src/Foundatio/Utility/TimeUnit.cs @@ -1,80 +1,77 @@ using System; -namespace Foundatio.Utility +namespace Foundatio.Utility; + +public static class TimeUnit { - public static class TimeUnit + public static TimeSpan Parse(string value) { - public static TimeSpan Parse(string value) - { - if (String.IsNullOrEmpty(value)) - throw new ArgumentNullException(nameof(value)); + if (String.IsNullOrEmpty(value)) + throw new ArgumentNullException(nameof(value)); - var time = ParseTime(value); - if (time.HasValue) - return time.Value; + var time = ParseTime(value); + if (time.HasValue) + return time.Value; - throw new ArgumentException($"Unable to parse value '{value}' as a valid time value"); - } + throw new ArgumentException($"Unable to parse value '{value}' as a valid time value"); + } - public static bool TryParse(string value, out TimeSpan? time) - { - time = null; - if (String.IsNullOrEmpty(value)) - return false; + public static bool TryParse(string value, out TimeSpan? time) + { + time = null; + if (String.IsNullOrEmpty(value)) + return false; - time = ParseTime(value); - return time.HasValue; - } + time = ParseTime(value); + return time.HasValue; + } - private static TimeSpan? ParseTime(string value) + private static TimeSpan? ParseTime(string value) + { + // compare using the original value as uppercase M could mean months. + var normalized = value.ToLowerInvariant().Trim(); + if (value.EndsWith("m")) { - // compare using the original value as uppercase M could mean months. - var normalized = value.ToLowerInvariant().Trim(); - if (value.EndsWith("m")) - { - int minutes = Int32.Parse(normalized.Substring(0, normalized.Length - 1)); - return new TimeSpan(0, minutes, 0); - } - - if (normalized.EndsWith("h")) - { - int hours = Int32.Parse(normalized.Substring(0, normalized.Length - 1)); - return new TimeSpan(hours, 0, 0); - } + int minutes = Int32.Parse(normalized.Substring(0, normalized.Length - 1)); + return new TimeSpan(0, minutes, 0); + } - if (normalized.EndsWith("d")) - { - int days = Int32.Parse(normalized.Substring(0, normalized.Length - 1)); - return new TimeSpan(days, 0, 0, 0); - } + if (normalized.EndsWith("h")) + { + int hours = Int32.Parse(normalized.Substring(0, normalized.Length - 1)); + return new TimeSpan(hours, 0, 0); + } - if (normalized.EndsWith("nanos")) - { - long nanoseconds = Int64.Parse(normalized.Substring(0, normalized.Length - 5)); - return new TimeSpan((int)Math.Round(nanoseconds / 100d)); - } + if (normalized.EndsWith("d")) + { + int days = Int32.Parse(normalized.Substring(0, normalized.Length - 1)); + return new TimeSpan(days, 0, 0, 0); + } - if (normalized.EndsWith("micros")) - { - long microseconds = Int64.Parse(normalized.Substring(0, normalized.Length - 6)); - return new TimeSpan(microseconds * 10); - } + if (normalized.EndsWith("nanos")) + { + long nanoseconds = Int64.Parse(normalized.Substring(0, normalized.Length - 5)); + return new TimeSpan((int)Math.Round(nanoseconds / 100d)); + } - if (normalized.EndsWith("ms")) - { - int milliseconds = Int32.Parse(normalized.Substring(0, normalized.Length - 2)); - return new TimeSpan(0, 0, 0, 0, milliseconds); - } + if (normalized.EndsWith("micros")) + { + long microseconds = Int64.Parse(normalized.Substring(0, normalized.Length - 6)); + return new TimeSpan(microseconds * 10); + } - if (normalized.EndsWith("s")) - { - int seconds = Int32.Parse(normalized.Substring(0, normalized.Length - 1)); - return new TimeSpan(0, 0, seconds); - } + if (normalized.EndsWith("ms")) + { + int milliseconds = Int32.Parse(normalized.Substring(0, normalized.Length - 2)); + return new TimeSpan(0, 0, 0, 0, milliseconds); + } - return null; + if (normalized.EndsWith("s")) + { + int seconds = Int32.Parse(normalized.Substring(0, normalized.Length - 1)); + return new TimeSpan(0, 0, seconds); } + + return null; } } - - diff --git a/src/Foundatio/Utility/TypeHelper.cs b/src/Foundatio/Utility/TypeHelper.cs index 2261b41fc..538c7a62f 100644 --- a/src/Foundatio/Utility/TypeHelper.cs +++ b/src/Foundatio/Utility/TypeHelper.cs @@ -5,137 +5,136 @@ using System.Reflection; using Microsoft.Extensions.Logging; -namespace Foundatio.Utility +namespace Foundatio.Utility; + +public static class TypeHelper { - public static class TypeHelper + public static readonly Type ObjectType = typeof(object); + public static readonly Type StringType = typeof(string); + public static readonly Type CharType = typeof(char); + public static readonly Type NullableCharType = typeof(char?); + public static readonly Type DateTimeType = typeof(DateTime); + public static readonly Type NullableDateTimeType = typeof(DateTime?); + public static readonly Type BoolType = typeof(bool); + public static readonly Type NullableBoolType = typeof(bool?); + public static readonly Type ByteArrayType = typeof(byte[]); + public static readonly Type ByteType = typeof(byte); + public static readonly Type SByteType = typeof(sbyte); + public static readonly Type SingleType = typeof(float); + public static readonly Type DecimalType = typeof(decimal); + public static readonly Type Int16Type = typeof(short); + public static readonly Type UInt16Type = typeof(ushort); + public static readonly Type Int32Type = typeof(int); + public static readonly Type UInt32Type = typeof(uint); + public static readonly Type Int64Type = typeof(long); + public static readonly Type UInt64Type = typeof(ulong); + public static readonly Type DoubleType = typeof(double); + + public static Type ResolveType(string fullTypeName, Type expectedBase = null, ILogger logger = null) { - public static readonly Type ObjectType = typeof(object); - public static readonly Type StringType = typeof(string); - public static readonly Type CharType = typeof(char); - public static readonly Type NullableCharType = typeof(char?); - public static readonly Type DateTimeType = typeof(DateTime); - public static readonly Type NullableDateTimeType = typeof(DateTime?); - public static readonly Type BoolType = typeof(bool); - public static readonly Type NullableBoolType = typeof(bool?); - public static readonly Type ByteArrayType = typeof(byte[]); - public static readonly Type ByteType = typeof(byte); - public static readonly Type SByteType = typeof(sbyte); - public static readonly Type SingleType = typeof(float); - public static readonly Type DecimalType = typeof(decimal); - public static readonly Type Int16Type = typeof(short); - public static readonly Type UInt16Type = typeof(ushort); - public static readonly Type Int32Type = typeof(int); - public static readonly Type UInt32Type = typeof(uint); - public static readonly Type Int64Type = typeof(long); - public static readonly Type UInt64Type = typeof(ulong); - public static readonly Type DoubleType = typeof(double); - - public static Type ResolveType(string fullTypeName, Type expectedBase = null, ILogger logger = null) - { - if (String.IsNullOrEmpty(fullTypeName)) - return null; - - var type = Type.GetType(fullTypeName); - if (type == null) - { - if (logger != null && logger.IsEnabled(LogLevel.Error)) - logger.LogError("Unable to resolve type: {TypeFullName}.", fullTypeName); - return null; - } + if (String.IsNullOrEmpty(fullTypeName)) + return null; - if (expectedBase != null && !expectedBase.IsAssignableFrom(type)) - { - if (logger != null && logger.IsEnabled(LogLevel.Error)) - logger.LogError("Type {TypeFullName} must be assignable to type: {ExpectedFullName}.", fullTypeName, expectedBase.FullName); - return null; - } + var type = Type.GetType(fullTypeName); + if (type == null) + { + if (logger != null && logger.IsEnabled(LogLevel.Error)) + logger.LogError("Unable to resolve type: {TypeFullName}.", fullTypeName); + return null; + } - return type; + if (expectedBase != null && !expectedBase.IsAssignableFrom(type)) + { + if (logger != null && logger.IsEnabled(LogLevel.Error)) + logger.LogError("Type {TypeFullName} must be assignable to type: {ExpectedFullName}.", fullTypeName, expectedBase.FullName); + return null; } - private static readonly Dictionary _builtInTypeNames = new() { - { StringType, "string" }, - { BoolType, "bool" }, - { ByteType, "byte" }, - { SByteType, "sbyte" }, - { CharType, "char" }, - { DecimalType, "decimal" }, - { DoubleType, "double" }, - { SingleType, "float" }, - { Int16Type, "short" }, - { Int32Type, "int" }, - { Int64Type, "long" }, - { ObjectType, "object" }, - { UInt16Type, "ushort" }, - { UInt32Type, "uint" }, - { UInt64Type, "ulong" } - }; - - public static string GetTypeDisplayName(Type type) + return type; + } + + private static readonly Dictionary _builtInTypeNames = new() { + { StringType, "string" }, + { BoolType, "bool" }, + { ByteType, "byte" }, + { SByteType, "sbyte" }, + { CharType, "char" }, + { DecimalType, "decimal" }, + { DoubleType, "double" }, + { SingleType, "float" }, + { Int16Type, "short" }, + { Int32Type, "int" }, + { Int64Type, "long" }, + { ObjectType, "object" }, + { UInt16Type, "ushort" }, + { UInt32Type, "uint" }, + { UInt64Type, "ulong" } + }; + + public static string GetTypeDisplayName(Type type) + { + string fullName = null; + if (type.GetTypeInfo().IsGenericType) { - string fullName = null; - if (type.GetTypeInfo().IsGenericType) - { - fullName = type.GetGenericTypeDefinition().FullName; + fullName = type.GetGenericTypeDefinition().FullName; - // Nested types (public or private) have a '+' in their full name - var parts = fullName.Split('+'); + // Nested types (public or private) have a '+' in their full name + var parts = fullName.Split('+'); + + // Handle nested generic types + // Examples: + // ConsoleApp.Program+Foo`1+Bar + // ConsoleApp.Program+Foo`1+Bar`1 + for (int i = 0; i < parts.Length; i++) + { + string partName = parts[i]; - // Handle nested generic types - // Examples: - // ConsoleApp.Program+Foo`1+Bar - // ConsoleApp.Program+Foo`1+Bar`1 - for (int i = 0; i < parts.Length; i++) + int backTickIndex = partName.IndexOf('`'); + if (backTickIndex >= 0) { - string partName = parts[i]; - - int backTickIndex = partName.IndexOf('`'); - if (backTickIndex >= 0) - { - // Since '.' is typically used to filter log messages in a hierarchy kind of scenario, - // do not include any generic type information as part of the name. - // Example: - // Microsoft.AspNetCore.Mvc -> log level set as Warning - // Microsoft.AspNetCore.Mvc.ModelBinding -> log level set as Verbose - partName = partName.Substring(0, backTickIndex); - } - - parts[i] = partName; + // Since '.' is typically used to filter log messages in a hierarchy kind of scenario, + // do not include any generic type information as part of the name. + // Example: + // Microsoft.AspNetCore.Mvc -> log level set as Warning + // Microsoft.AspNetCore.Mvc.ModelBinding -> log level set as Verbose + partName = partName.Substring(0, backTickIndex); } - return String.Join(".", parts); + parts[i] = partName; } - if (_builtInTypeNames.ContainsKey(type)) - return _builtInTypeNames[type]; + return String.Join(".", parts); + } - fullName = type.FullName; - if (type.IsNested) - fullName = fullName.Replace('+', '.'); + if (_builtInTypeNames.ContainsKey(type)) + return _builtInTypeNames[type]; - return fullName; - } + fullName = type.FullName; + if (type.IsNested) + fullName = fullName.Replace('+', '.'); - public static IEnumerable GetDerivedTypes(IEnumerable assemblies = null) - { - if (assemblies == null) - assemblies = AppDomain.CurrentDomain.GetAssemblies(); + return fullName; + } - var types = new List(); - foreach (var assembly in assemblies) + public static IEnumerable GetDerivedTypes(IEnumerable assemblies = null) + { + if (assemblies == null) + assemblies = AppDomain.CurrentDomain.GetAssemblies(); + + var types = new List(); + foreach (var assembly in assemblies) + { + try { - try - { - types.AddRange(from type in assembly.GetTypes() where type.IsClass && !type.IsNotPublic && !type.IsAbstract && typeof(TAction).IsAssignableFrom(type) select type); - } - catch (ReflectionTypeLoadException ex) - { - string loaderMessages = String.Join(", ", ex.LoaderExceptions.ToList().Select(le => le.Message)); - Trace.TraceInformation("Unable to search types from assembly \"{0}\" for plugins of type \"{1}\": {2}", assembly.FullName, typeof(TAction).Name, loaderMessages); - } + types.AddRange(from type in assembly.GetTypes() where type.IsClass && !type.IsNotPublic && !type.IsAbstract && typeof(TAction).IsAssignableFrom(type) select type); + } + catch (ReflectionTypeLoadException ex) + { + string loaderMessages = String.Join(", ", ex.LoaderExceptions.ToList().Select(le => le.Message)); + Trace.TraceInformation("Unable to search types from assembly \"{0}\" for plugins of type \"{1}\": {2}", assembly.FullName, typeof(TAction).Name, loaderMessages); } - - return types; } + + return types; } } diff --git a/tests/Foundatio.Tests/Caching/InMemoryCacheClientTests.cs b/tests/Foundatio.Tests/Caching/InMemoryCacheClientTests.cs index d992d0e5d..7dc699ebe 100644 --- a/tests/Foundatio.Tests/Caching/InMemoryCacheClientTests.cs +++ b/tests/Foundatio.Tests/Caching/InMemoryCacheClientTests.cs @@ -7,198 +7,197 @@ using Xunit; using Xunit.Abstractions; -namespace Foundatio.Tests.Caching +namespace Foundatio.Tests.Caching; + +public class InMemoryCacheClientTests : CacheClientTestsBase { - public class InMemoryCacheClientTests : CacheClientTestsBase + public InMemoryCacheClientTests(ITestOutputHelper output) : base(output) { } + + protected override ICacheClient GetCacheClient(bool shouldThrowOnSerializationError = true) { - public InMemoryCacheClientTests(ITestOutputHelper output) : base(output) { } + return new InMemoryCacheClient(o => o.LoggerFactory(Log).CloneValues(true).ShouldThrowOnSerializationError(shouldThrowOnSerializationError)); + } - protected override ICacheClient GetCacheClient(bool shouldThrowOnSerializationError = true) - { - return new InMemoryCacheClient(o => o.LoggerFactory(Log).CloneValues(true).ShouldThrowOnSerializationError(shouldThrowOnSerializationError)); - } + [Fact] + public override Task CanGetAllAsync() + { + return base.CanGetAllAsync(); + } - [Fact] - public override Task CanGetAllAsync() - { - return base.CanGetAllAsync(); - } + [Fact] + public override Task CanGetAllWithOverlapAsync() + { + return base.CanGetAllWithOverlapAsync(); + } - [Fact] - public override Task CanGetAllWithOverlapAsync() - { - return base.CanGetAllWithOverlapAsync(); - } + [Fact] + public override Task CanSetAsync() + { + return base.CanSetAsync(); + } - [Fact] - public override Task CanSetAsync() - { - return base.CanSetAsync(); - } + [Fact] + public override Task CanSetAndGetValueAsync() + { + return base.CanSetAndGetValueAsync(); + } - [Fact] - public override Task CanSetAndGetValueAsync() - { - return base.CanSetAndGetValueAsync(); - } + [Fact] + public override Task CanAddAsync() + { + return base.CanAddAsync(); + } - [Fact] - public override Task CanAddAsync() - { - return base.CanAddAsync(); - } + [Fact] + public override Task CanAddConcurrentlyAsync() + { + return base.CanAddConcurrentlyAsync(); + } - [Fact] - public override Task CanAddConcurrentlyAsync() - { - return base.CanAddConcurrentlyAsync(); - } + [Fact] + public override Task CanGetAsync() + { + return base.CanGetAsync(); + } - [Fact] - public override Task CanGetAsync() - { - return base.CanGetAsync(); - } + [Fact] + public override Task CanTryGetAsync() + { + return base.CanTryGetAsync(); + } - [Fact] - public override Task CanTryGetAsync() - { - return base.CanTryGetAsync(); - } + [Fact] + public override Task CanUseScopedCachesAsync() + { + return base.CanUseScopedCachesAsync(); + } - [Fact] - public override Task CanUseScopedCachesAsync() - { - return base.CanUseScopedCachesAsync(); - } + [Fact] + public override Task CanSetAndGetObjectAsync() + { + return base.CanSetAndGetObjectAsync(); + } - [Fact] - public override Task CanSetAndGetObjectAsync() - { - return base.CanSetAndGetObjectAsync(); - } + [Fact] + public override Task CanRemoveByPrefixAsync() + { + return base.CanRemoveByPrefixAsync(); + } - [Fact] - public override Task CanRemoveByPrefixAsync() - { - return base.CanRemoveByPrefixAsync(); - } + [Theory] + [InlineData(50)] + [InlineData(500)] + public override Task CanRemoveByPrefixMultipleEntriesAsync(int count) + { + return base.CanRemoveByPrefixMultipleEntriesAsync(count); + } - [Theory] - [InlineData(50)] - [InlineData(500)] - public override Task CanRemoveByPrefixMultipleEntriesAsync(int count) - { - return base.CanRemoveByPrefixMultipleEntriesAsync(count); - } + [Fact] + public override Task CanSetExpirationAsync() + { + return base.CanSetExpirationAsync(); + } - [Fact] - public override Task CanSetExpirationAsync() - { - return base.CanSetExpirationAsync(); - } + [Fact] + public override Task CanSetMinMaxExpirationAsync() + { + return base.CanSetMinMaxExpirationAsync(); + } - [Fact] - public override Task CanSetMinMaxExpirationAsync() - { - return base.CanSetMinMaxExpirationAsync(); - } + [Fact] + public override Task CanIncrementAsync() + { + return base.CanIncrementAsync(); + } - [Fact] - public override Task CanIncrementAsync() - { - return base.CanIncrementAsync(); - } + [Fact] + public override Task CanIncrementAndExpireAsync() + { + return base.CanIncrementAndExpireAsync(); + } - [Fact] - public override Task CanIncrementAndExpireAsync() - { - return base.CanIncrementAndExpireAsync(); - } + [Fact] + public override Task CanReplaceIfEqual() + { + return base.CanReplaceIfEqual(); + } - [Fact] - public override Task CanReplaceIfEqual() - { - return base.CanReplaceIfEqual(); - } + [Fact] + public override Task CanRemoveIfEqual() + { + return base.CanRemoveIfEqual(); + } - [Fact] - public override Task CanRemoveIfEqual() - { - return base.CanRemoveIfEqual(); - } + [Fact] + public override Task CanGetAndSetDateTimeAsync() + { + return base.CanGetAndSetDateTimeAsync(); + } - [Fact] - public override Task CanGetAndSetDateTimeAsync() - { - return base.CanGetAndSetDateTimeAsync(); - } + [Fact] + public override Task CanRoundTripLargeNumbersAsync() + { + return base.CanRoundTripLargeNumbersAsync(); + } - [Fact] - public override Task CanRoundTripLargeNumbersAsync() - { - return base.CanRoundTripLargeNumbersAsync(); - } + [Fact] + public override Task CanRoundTripLargeNumbersWithExpirationAsync() + { + return base.CanRoundTripLargeNumbersWithExpirationAsync(); + } - [Fact] - public override Task CanRoundTripLargeNumbersWithExpirationAsync() - { - return base.CanRoundTripLargeNumbersWithExpirationAsync(); - } + [Fact] + public override Task CanManageListsAsync() + { + return base.CanManageListsAsync(); + } - [Fact] - public override Task CanManageListsAsync() - { - return base.CanManageListsAsync(); - } + [Fact] + public async Task CanSetMaxItems() + { + Log.MinimumLevel = LogLevel.Trace; - [Fact] - public async Task CanSetMaxItems() + // run in tight loop so that the code is warmed up and we can catch timing issues + for (int x = 0; x < 5; x++) { - Log.MinimumLevel = LogLevel.Trace; + var cache = new InMemoryCacheClient(o => o.MaxItems(10).CloneValues(true)); - // run in tight loop so that the code is warmed up and we can catch timing issues - for (int x = 0; x < 5; x++) + using (cache) { - var cache = new InMemoryCacheClient(o => o.MaxItems(10).CloneValues(true)); - - using (cache) - { - await cache.RemoveAllAsync(); - - for (int i = 0; i < cache.MaxItems; i++) - await cache.SetAsync("test" + i, i); - - _logger.LogTrace(String.Join(",", cache.Keys)); - Assert.Equal(10, cache.Count); - await cache.SetAsync("next", 1); - _logger.LogTrace(String.Join(",", cache.Keys)); - Assert.Equal(10, cache.Count); - Assert.False((await cache.GetAsync("test0")).HasValue); - Assert.Equal(1, cache.Misses); - await SystemClock.SleepAsync(50); // keep the last access ticks from being the same for all items - Assert.NotNull(await cache.GetAsync("test1")); - Assert.Equal(1, cache.Hits); - await cache.SetAsync("next2", 2); - _logger.LogTrace(String.Join(",", cache.Keys)); - Assert.False((await cache.GetAsync("test2")).HasValue); - Assert.Equal(2, cache.Misses); - Assert.True((await cache.GetAsync("test1")).HasValue); - Assert.Equal(2, cache.Misses); - } + await cache.RemoveAllAsync(); + + for (int i = 0; i < cache.MaxItems; i++) + await cache.SetAsync("test" + i, i); + + _logger.LogTrace(String.Join(",", cache.Keys)); + Assert.Equal(10, cache.Count); + await cache.SetAsync("next", 1); + _logger.LogTrace(String.Join(",", cache.Keys)); + Assert.Equal(10, cache.Count); + Assert.False((await cache.GetAsync("test0")).HasValue); + Assert.Equal(1, cache.Misses); + await SystemClock.SleepAsync(50); // keep the last access ticks from being the same for all items + Assert.NotNull(await cache.GetAsync("test1")); + Assert.Equal(1, cache.Hits); + await cache.SetAsync("next2", 2); + _logger.LogTrace(String.Join(",", cache.Keys)); + Assert.False((await cache.GetAsync("test2")).HasValue); + Assert.Equal(2, cache.Misses); + Assert.True((await cache.GetAsync("test1")).HasValue); + Assert.Equal(2, cache.Misses); } } + } - [Fact] - public async Task SetAllShouldExpire() - { - var client = GetCacheClient(); + [Fact] + public async Task SetAllShouldExpire() + { + var client = GetCacheClient(); - var expiry = TimeSpan.FromMilliseconds(50); - await client.SetAllAsync(new Dictionary { { "test", "value" } }, expiry); - await Task.Delay(expiry); + var expiry = TimeSpan.FromMilliseconds(50); + await client.SetAllAsync(new Dictionary { { "test", "value" } }, expiry); + await Task.Delay(expiry); - Assert.False(await client.ExistsAsync("test")); - } + Assert.False(await client.ExistsAsync("test")); } } diff --git a/tests/Foundatio.Tests/Caching/InMemoryHybridCacheClientTests.cs b/tests/Foundatio.Tests/Caching/InMemoryHybridCacheClientTests.cs index 1fc86637d..d2e9b197b 100644 --- a/tests/Foundatio.Tests/Caching/InMemoryHybridCacheClientTests.cs +++ b/tests/Foundatio.Tests/Caching/InMemoryHybridCacheClientTests.cs @@ -5,121 +5,120 @@ using Xunit; using Xunit.Abstractions; -namespace Foundatio.Tests.Caching +namespace Foundatio.Tests.Caching; + +public class InMemoryHybridCacheClientTests : HybridCacheClientTests { - public class InMemoryHybridCacheClientTests : HybridCacheClientTests + public InMemoryHybridCacheClientTests(ITestOutputHelper output) : base(output) { } + + protected override ICacheClient GetCacheClient(bool shouldThrowOnSerializationError = true) { - public InMemoryHybridCacheClientTests(ITestOutputHelper output) : base(output) { } + return new InMemoryHybridCacheClient(_messageBus, Log, shouldThrowOnSerializationError); + } - protected override ICacheClient GetCacheClient(bool shouldThrowOnSerializationError = true) - { - return new InMemoryHybridCacheClient(_messageBus, Log, shouldThrowOnSerializationError); - } + [Fact] + public override Task CanSetAndGetValueAsync() + { + return base.CanSetAndGetValueAsync(); + } - [Fact] - public override Task CanSetAndGetValueAsync() - { - return base.CanSetAndGetValueAsync(); - } + [Fact] + public override Task CanSetAndGetObjectAsync() + { + return base.CanSetAndGetObjectAsync(); + } - [Fact] - public override Task CanSetAndGetObjectAsync() - { - return base.CanSetAndGetObjectAsync(); - } + [Fact] + public override Task CanTryGetAsync() + { + return base.CanTryGetAsync(); + } - [Fact] - public override Task CanTryGetAsync() - { - return base.CanTryGetAsync(); - } + [Fact] + public override Task CanRemoveByPrefixAsync() + { + return base.CanRemoveByPrefixAsync(); + } - [Fact] - public override Task CanRemoveByPrefixAsync() - { - return base.CanRemoveByPrefixAsync(); - } + [Theory] + [InlineData(50)] + [InlineData(500)] + public override Task CanRemoveByPrefixMultipleEntriesAsync(int count) + { + return base.CanRemoveByPrefixMultipleEntriesAsync(count); + } - [Theory] - [InlineData(50)] - [InlineData(500)] - public override Task CanRemoveByPrefixMultipleEntriesAsync(int count) - { - return base.CanRemoveByPrefixMultipleEntriesAsync(count); - } + [Fact] + public override Task CanUseScopedCachesAsync() + { + return base.CanUseScopedCachesAsync(); + } - [Fact] - public override Task CanUseScopedCachesAsync() - { - return base.CanUseScopedCachesAsync(); - } + [Fact] + public override Task CanSetExpirationAsync() + { + return base.CanSetExpirationAsync(); + } - [Fact] - public override Task CanSetExpirationAsync() - { - return base.CanSetExpirationAsync(); - } + [Fact] + public override Task CanManageListsAsync() + { + return base.CanManageListsAsync(); + } - [Fact] - public override Task CanManageListsAsync() - { - return base.CanManageListsAsync(); - } + [Fact(Skip = "Skip because cache invalidation loops on this with 2 in memory cache client instances")] + public override Task WillUseLocalCache() + { + return base.WillUseLocalCache(); + } - [Fact(Skip = "Skip because cache invalidation loops on this with 2 in memory cache client instances")] - public override Task WillUseLocalCache() - { - return base.WillUseLocalCache(); - } + [Fact(Skip = "Skip because cache invalidation loops on this with 2 in memory cache client instances")] + public override Task WillExpireRemoteItems() + { + return base.WillExpireRemoteItems(); + } - [Fact(Skip = "Skip because cache invalidation loops on this with 2 in memory cache client instances")] - public override Task WillExpireRemoteItems() - { - return base.WillExpireRemoteItems(); - } + [Fact(Skip = "Skip because cache invalidation loops on this with 2 in memory cache client instances")] + public override Task WillWorkWithSets() + { + Log.MinimumLevel = LogLevel.Trace; + return base.WillWorkWithSets(); + } - [Fact(Skip = "Skip because cache invalidation loops on this with 2 in memory cache client instances")] - public override Task WillWorkWithSets() - { - Log.MinimumLevel = LogLevel.Trace; - return base.WillWorkWithSets(); - } + [Fact(Skip = "Performance Test")] + public override Task MeasureThroughputAsync() + { + return base.MeasureThroughputAsync(); + } - [Fact(Skip = "Performance Test")] - public override Task MeasureThroughputAsync() - { - return base.MeasureThroughputAsync(); - } + [Fact(Skip = "Performance Test")] + public override Task MeasureSerializerSimpleThroughputAsync() + { + return base.MeasureSerializerSimpleThroughputAsync(); + } - [Fact(Skip = "Performance Test")] - public override Task MeasureSerializerSimpleThroughputAsync() - { - return base.MeasureSerializerSimpleThroughputAsync(); - } + [Fact(Skip = "Performance Test")] + public override Task MeasureSerializerComplexThroughputAsync() + { + return base.MeasureSerializerComplexThroughputAsync(); + } +} - [Fact(Skip = "Performance Test")] - public override Task MeasureSerializerComplexThroughputAsync() +public class InMemoryHybridCacheClient : HybridCacheClient +{ + public InMemoryHybridCacheClient(IMessageBus messageBus, ILoggerFactory loggerFactory, bool shouldThrowOnSerializationError) + : base(new InMemoryCacheClient(o => o.LoggerFactory(loggerFactory).ShouldThrowOnSerializationError(shouldThrowOnSerializationError)), messageBus, new InMemoryCacheClientOptions { - return base.MeasureSerializerComplexThroughputAsync(); - } + CloneValues = true, + ShouldThrowOnSerializationError = shouldThrowOnSerializationError + }, loggerFactory) + { } - public class InMemoryHybridCacheClient : HybridCacheClient + public override void Dispose() { - public InMemoryHybridCacheClient(IMessageBus messageBus, ILoggerFactory loggerFactory, bool shouldThrowOnSerializationError) - : base(new InMemoryCacheClient(o => o.LoggerFactory(loggerFactory).ShouldThrowOnSerializationError(shouldThrowOnSerializationError)), messageBus, new InMemoryCacheClientOptions - { - CloneValues = true, - ShouldThrowOnSerializationError = shouldThrowOnSerializationError - }, loggerFactory) - { - } - - public override void Dispose() - { - base.Dispose(); - _distributedCache.Dispose(); - _messageBus.Dispose(); - } + base.Dispose(); + _distributedCache.Dispose(); + _messageBus.Dispose(); } } diff --git a/tests/Foundatio.Tests/Hosting/HostingTests.cs b/tests/Foundatio.Tests/Hosting/HostingTests.cs index 1962ac288..2b81859aa 100644 --- a/tests/Foundatio.Tests/Hosting/HostingTests.cs +++ b/tests/Foundatio.Tests/Hosting/HostingTests.cs @@ -14,158 +14,157 @@ using Xunit; using Xunit.Abstractions; -namespace Foundatio.Tests.Hosting -{ - public class HostingTests : TestWithLoggingBase - { - public HostingTests(ITestOutputHelper output) : base(output) { } - - [Fact] - public async Task WillRunSyncStartupAction() - { - var resetEvent = new AsyncManualResetEvent(false); - var builder = new WebHostBuilder() - .ConfigureServices(s => - { - s.AddSingleton(Log); - s.AddStartupAction("Hey", () => resetEvent.Set()); - s.AddHealthChecks().AddCheckForStartupActions("Critical"); - }) - .Configure(app => - { - app.UseReadyHealthChecks("Critical"); - }); - - var server = new TestServer(builder); +namespace Foundatio.Tests.Hosting; - await server.WaitForReadyAsync(); - await resetEvent.WaitAsync(); - - var client = server.CreateClient(); - var response = await client.GetAsync("/ready"); - - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - } - - [Fact] - public async Task WillRunAsyncStartupAction() - { - var resetEvent = new AsyncManualResetEvent(false); - var builder = new WebHostBuilder() - .ConfigureServices(s => - { - s.AddSingleton(Log); - s.AddStartupAction("Hey", () => - { - resetEvent.Set(); - return Task.CompletedTask; - }); - s.AddHealthChecks().AddCheckForStartupActions("Critical"); - }) - .Configure(app => - { - app.UseReadyHealthChecks("Critical"); - }); - - var server = new TestServer(builder); - - await server.WaitForReadyAsync(); - await resetEvent.WaitAsync(); - - var client = server.CreateClient(); - var response = await client.GetAsync("/ready"); +public class HostingTests : TestWithLoggingBase +{ + public HostingTests(ITestOutputHelper output) : base(output) { } - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - } + [Fact] + public async Task WillRunSyncStartupAction() + { + var resetEvent = new AsyncManualResetEvent(false); + var builder = new WebHostBuilder() + .ConfigureServices(s => + { + s.AddSingleton(Log); + s.AddStartupAction("Hey", () => resetEvent.Set()); + s.AddHealthChecks().AddCheckForStartupActions("Critical"); + }) + .Configure(app => + { + app.UseReadyHealthChecks("Critical"); + }); + + var server = new TestServer(builder); + + await server.WaitForReadyAsync(); + await resetEvent.WaitAsync(); + + var client = server.CreateClient(); + var response = await client.GetAsync("/ready"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } - [Fact] - public async Task WillRunClassStartupAction() - { - var builder = new WebHostBuilder() - .ConfigureServices(s => - { - s.AddSingleton(Log); - s.AddStartupAction("Hey"); - s.AddHealthChecks().AddCheckForStartupActions("Critical"); - }) - .Configure(app => + [Fact] + public async Task WillRunAsyncStartupAction() + { + var resetEvent = new AsyncManualResetEvent(false); + var builder = new WebHostBuilder() + .ConfigureServices(s => + { + s.AddSingleton(Log); + s.AddStartupAction("Hey", () => { - app.UseReadyHealthChecks("Critical"); + resetEvent.Set(); + return Task.CompletedTask; }); + s.AddHealthChecks().AddCheckForStartupActions("Critical"); + }) + .Configure(app => + { + app.UseReadyHealthChecks("Critical"); + }); - var server = new TestServer(builder); - - await server.WaitForReadyAsync(); - Assert.True(TestStartupAction.HasRun); + var server = new TestServer(builder); - var client = server.CreateClient(); - var response = await client.GetAsync("/ready"); + await server.WaitForReadyAsync(); + await resetEvent.WaitAsync(); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - } + var client = server.CreateClient(); + var response = await client.GetAsync("/ready"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } - [Fact] - public async Task WillStopWaitingWhenStartupActionFails() - { - var builder = new WebHostBuilder() - .CaptureStartupErrors(true) - .ConfigureServices(s => - { - s.AddSingleton(Log); - s.AddStartupAction("Boom", () => throw new ApplicationException("Boom")); - s.AddHealthChecks().AddCheckForStartupActions("Critical"); - }) - .Configure(app => - { - app.UseReadyHealthChecks("Critical"); - }); - - var server = new TestServer(builder); - - var sw = Stopwatch.StartNew(); - await Assert.ThrowsAsync(() => server.WaitForReadyAsync()); - sw.Stop(); - - Assert.True(sw.Elapsed < TimeSpan.FromSeconds(2)); - } + [Fact] + public async Task WillRunClassStartupAction() + { + var builder = new WebHostBuilder() + .ConfigureServices(s => + { + s.AddSingleton(Log); + s.AddStartupAction("Hey"); + s.AddHealthChecks().AddCheckForStartupActions("Critical"); + }) + .Configure(app => + { + app.UseReadyHealthChecks("Critical"); + }); + + var server = new TestServer(builder); + + await server.WaitForReadyAsync(); + Assert.True(TestStartupAction.HasRun); + + var client = server.CreateClient(); + var response = await client.GetAsync("/ready"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } - [Fact] - public async Task WillHandleNoRegisteredStartupActions() - { - var builder = new WebHostBuilder() - .UseEnvironment(Environments.Development) - .CaptureStartupErrors(true) - .ConfigureServices(s => - { - s.AddSingleton(Log); - s.AddHealthChecks().AddCheckForStartupActions("Critical"); - }) - .Configure(app => - { - app.UseReadyHealthChecks("Critical"); - app.UseWaitForStartupActionsBeforeServingRequests(); - }); - var server = new TestServer(builder); + [Fact] + public async Task WillStopWaitingWhenStartupActionFails() + { + var builder = new WebHostBuilder() + .CaptureStartupErrors(true) + .ConfigureServices(s => + { + s.AddSingleton(Log); + s.AddStartupAction("Boom", () => throw new ApplicationException("Boom")); + s.AddHealthChecks().AddCheckForStartupActions("Critical"); + }) + .Configure(app => + { + app.UseReadyHealthChecks("Critical"); + }); + + var server = new TestServer(builder); + + var sw = Stopwatch.StartNew(); + await Assert.ThrowsAsync(() => server.WaitForReadyAsync()); + sw.Stop(); + + Assert.True(sw.Elapsed < TimeSpan.FromSeconds(2)); + } - var sw = Stopwatch.StartNew(); - await server.WaitForReadyAsync(); - sw.Stop(); + [Fact] + public async Task WillHandleNoRegisteredStartupActions() + { + var builder = new WebHostBuilder() + .UseEnvironment(Environments.Development) + .CaptureStartupErrors(true) + .ConfigureServices(s => + { + s.AddSingleton(Log); + s.AddHealthChecks().AddCheckForStartupActions("Critical"); + }) + .Configure(app => + { + app.UseReadyHealthChecks("Critical"); + app.UseWaitForStartupActionsBeforeServingRequests(); + }); + + var server = new TestServer(builder); + + var sw = Stopwatch.StartNew(); + await server.WaitForReadyAsync(); + sw.Stop(); + + Assert.True(sw.Elapsed < TimeSpan.FromSeconds(2)); + } - Assert.True(sw.Elapsed < TimeSpan.FromSeconds(2)); - } +} - } +public class TestStartupAction : IStartupAction +{ + public static bool HasRun { get; private set; } - public class TestStartupAction : IStartupAction + public Task RunAsync(CancellationToken shutdownToken = default) { - public static bool HasRun { get; private set; } - - public Task RunAsync(CancellationToken shutdownToken = default) - { - HasRun = true; - return Task.CompletedTask; - } + HasRun = true; + return Task.CompletedTask; } } diff --git a/tests/Foundatio.Tests/Hosting/TestServerExtensions.cs b/tests/Foundatio.Tests/Hosting/TestServerExtensions.cs index 5e7b0f9e6..551266cbc 100644 --- a/tests/Foundatio.Tests/Hosting/TestServerExtensions.cs +++ b/tests/Foundatio.Tests/Hosting/TestServerExtensions.cs @@ -4,31 +4,30 @@ using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; -namespace Foundatio.Tests.Hosting +namespace Foundatio.Tests.Hosting; + +public static class TestServerExtensions { - public static class TestServerExtensions + public static async Task WaitForReadyAsync(this TestServer server, TimeSpan? maxWaitTime = null) { - public static async Task WaitForReadyAsync(this TestServer server, TimeSpan? maxWaitTime = null) - { - var startupContext = server.Host.Services.GetService(); - maxWaitTime ??= TimeSpan.FromSeconds(5); + var startupContext = server.Host.Services.GetService(); + maxWaitTime ??= TimeSpan.FromSeconds(5); - var client = server.CreateClient(); - var startTime = DateTime.Now; - do - { - if (startupContext != null && startupContext.IsStartupComplete && startupContext.Result.Success == false) - throw new OperationCanceledException($"Startup action \"{startupContext.Result.FailedActionName}\" failed"); + var client = server.CreateClient(); + var startTime = DateTime.Now; + do + { + if (startupContext != null && startupContext.IsStartupComplete && startupContext.Result.Success == false) + throw new OperationCanceledException($"Startup action \"{startupContext.Result.FailedActionName}\" failed"); - var response = await client.GetAsync("/ready"); - if (response.IsSuccessStatusCode) - break; + var response = await client.GetAsync("/ready"); + if (response.IsSuccessStatusCode) + break; - if (DateTime.Now.Subtract(startTime) > maxWaitTime) - throw new TimeoutException("Failed waiting for server to be ready"); + if (DateTime.Now.Subtract(startTime) > maxWaitTime) + throw new TimeoutException("Failed waiting for server to be ready"); - await Task.Delay(TimeSpan.FromMilliseconds(100)); - } while (true); - } + await Task.Delay(TimeSpan.FromMilliseconds(100)); + } while (true); } } diff --git a/tests/Foundatio.Tests/Jobs/InMemoryJobQueueTests.cs b/tests/Foundatio.Tests/Jobs/InMemoryJobQueueTests.cs index 223e65ac9..31c0bb4a5 100644 --- a/tests/Foundatio.Tests/Jobs/InMemoryJobQueueTests.cs +++ b/tests/Foundatio.Tests/Jobs/InMemoryJobQueueTests.cs @@ -8,41 +8,40 @@ using Xunit; using Xunit.Abstractions; -namespace Foundatio.Tests.Jobs +namespace Foundatio.Tests.Jobs; + +public class InMemoryJobQueueTests : JobQueueTestsBase { - public class InMemoryJobQueueTests : JobQueueTestsBase + public InMemoryJobQueueTests(ITestOutputHelper output) : base(output) { } + + protected override IQueue GetSampleWorkItemQueue(int retries, TimeSpan retryDelay) + { + return new InMemoryQueue(o => o.RetryDelay(retryDelay).Retries(retries).LoggerFactory(Log)); + } + + [Fact] + public override Task CanRunMultipleQueueJobsAsync() + { + return base.CanRunMultipleQueueJobsAsync(); + } + + [RetryFact] + public override Task CanRunQueueJobWithLockFailAsync() + { + Log.SetLogLevel(LogLevel.Trace); + + return base.CanRunQueueJobWithLockFailAsync(); + } + + [Fact] + public override Task CanRunQueueJobAsync() + { + return base.CanRunQueueJobAsync(); + } + + [Fact] + public override Task ActivityWillFlowThroughQueueJobAsync() { - public InMemoryJobQueueTests(ITestOutputHelper output) : base(output) { } - - protected override IQueue GetSampleWorkItemQueue(int retries, TimeSpan retryDelay) - { - return new InMemoryQueue(o => o.RetryDelay(retryDelay).Retries(retries).LoggerFactory(Log)); - } - - [Fact] - public override Task CanRunMultipleQueueJobsAsync() - { - return base.CanRunMultipleQueueJobsAsync(); - } - - [RetryFact] - public override Task CanRunQueueJobWithLockFailAsync() - { - Log.SetLogLevel(LogLevel.Trace); - - return base.CanRunQueueJobWithLockFailAsync(); - } - - [Fact] - public override Task CanRunQueueJobAsync() - { - return base.CanRunQueueJobAsync(); - } - - [Fact] - public override Task ActivityWillFlowThroughQueueJobAsync() - { - return base.ActivityWillFlowThroughQueueJobAsync(); - } + return base.ActivityWillFlowThroughQueueJobAsync(); } } diff --git a/tests/Foundatio.Tests/Jobs/JobTests.cs b/tests/Foundatio.Tests/Jobs/JobTests.cs index b94878032..a82cb52f8 100644 --- a/tests/Foundatio.Tests/Jobs/JobTests.cs +++ b/tests/Foundatio.Tests/Jobs/JobTests.cs @@ -13,197 +13,196 @@ using Xunit; using Xunit.Abstractions; -namespace Foundatio.Tests.Jobs +namespace Foundatio.Tests.Jobs; + +public class JobTests : TestWithLoggingBase { - public class JobTests : TestWithLoggingBase + public JobTests(ITestOutputHelper output) : base(output) { } + + [Fact] + public async Task CanCancelJob() { - public JobTests(ITestOutputHelper output) : base(output) { } + var job = new HelloWorldJob(Log); + var timeoutCancellationTokenSource = new CancellationTokenSource(1000); + var resultTask = new JobRunner(job, Log).RunAsync(timeoutCancellationTokenSource.Token); + await SystemClock.SleepAsync(TimeSpan.FromSeconds(2)); + Assert.True(await resultTask); + } - [Fact] - public async Task CanCancelJob() - { - var job = new HelloWorldJob(Log); - var timeoutCancellationTokenSource = new CancellationTokenSource(1000); - var resultTask = new JobRunner(job, Log).RunAsync(timeoutCancellationTokenSource.Token); - await SystemClock.SleepAsync(TimeSpan.FromSeconds(2)); - Assert.True(await resultTask); - } + [Fact] + public async Task CanStopLongRunningJob() + { + var job = new LongRunningJob(Log); + var runner = new JobRunner(job, Log); + var cts = new CancellationTokenSource(1000); + bool result = await runner.RunAsync(cts.Token); - [Fact] - public async Task CanStopLongRunningJob() - { - var job = new LongRunningJob(Log); - var runner = new JobRunner(job, Log); - var cts = new CancellationTokenSource(1000); - bool result = await runner.RunAsync(cts.Token); + Assert.True(result); + } - Assert.True(result); - } + [Fact] + public async Task CanStopLongRunningCronJob() + { + var job = new LongRunningJob(Log); + var runner = new JobRunner(job, Log); + var cts = new CancellationTokenSource(1000); + bool result = await runner.RunAsync(cts.Token); - [Fact] - public async Task CanStopLongRunningCronJob() - { - var job = new LongRunningJob(Log); - var runner = new JobRunner(job, Log); - var cts = new CancellationTokenSource(1000); - bool result = await runner.RunAsync(cts.Token); + Assert.True(result); + } - Assert.True(result); - } + [Fact] + public async Task CanRunJobs() + { + var job = new HelloWorldJob(Log); + Assert.Equal(0, job.RunCount); + await job.RunAsync(); + Assert.Equal(1, job.RunCount); + + await job.RunContinuousAsync(iterationLimit: 2); + Assert.Equal(3, job.RunCount); - [Fact] - public async Task CanRunJobs() + var sw = Stopwatch.StartNew(); + using (var timeoutCancellationTokenSource = new CancellationTokenSource(100)) { - var job = new HelloWorldJob(Log); - Assert.Equal(0, job.RunCount); - await job.RunAsync(); - Assert.Equal(1, job.RunCount); + await job.RunContinuousAsync(cancellationToken: timeoutCancellationTokenSource.Token); + } + sw.Stop(); + Assert.InRange(sw.Elapsed, TimeSpan.FromMilliseconds(95), TimeSpan.FromMilliseconds(800)); + + var jobInstance = new HelloWorldJob(Log); + Assert.NotNull(jobInstance); + Assert.Equal(0, jobInstance.RunCount); + Assert.Equal(JobResult.Success, await jobInstance.RunAsync()); + Assert.Equal(1, jobInstance.RunCount); + } + + [Fact] + public async Task CanRunMultipleInstances() + { + var job = new HelloWorldJob(Log); - await job.RunContinuousAsync(iterationLimit: 2); - Assert.Equal(3, job.RunCount); - - var sw = Stopwatch.StartNew(); - using (var timeoutCancellationTokenSource = new CancellationTokenSource(100)) - { - await job.RunContinuousAsync(cancellationToken: timeoutCancellationTokenSource.Token); - } - sw.Stop(); - Assert.InRange(sw.Elapsed, TimeSpan.FromMilliseconds(95), TimeSpan.FromMilliseconds(800)); - - var jobInstance = new HelloWorldJob(Log); - Assert.NotNull(jobInstance); - Assert.Equal(0, jobInstance.RunCount); - Assert.Equal(JobResult.Success, await jobInstance.RunAsync()); - Assert.Equal(1, jobInstance.RunCount); + HelloWorldJob.GlobalRunCount = 0; + using (var timeoutCancellationTokenSource = new CancellationTokenSource(1000)) + { + await new JobRunner(job, Log, instanceCount: 5, iterationLimit: 1).RunAsync(timeoutCancellationTokenSource.Token); } - [Fact] - public async Task CanRunMultipleInstances() + Assert.Equal(5, HelloWorldJob.GlobalRunCount); + + HelloWorldJob.GlobalRunCount = 0; + using (var timeoutCancellationTokenSource = new CancellationTokenSource(50000)) { - var job = new HelloWorldJob(Log); + await new JobRunner(job, Log, instanceCount: 5, iterationLimit: 100).RunAsync(timeoutCancellationTokenSource.Token); + } - HelloWorldJob.GlobalRunCount = 0; - using (var timeoutCancellationTokenSource = new CancellationTokenSource(1000)) - { - await new JobRunner(job, Log, instanceCount: 5, iterationLimit: 1).RunAsync(timeoutCancellationTokenSource.Token); - } + Assert.Equal(500, HelloWorldJob.GlobalRunCount); + } - Assert.Equal(5, HelloWorldJob.GlobalRunCount); + [Fact] + public async Task CanCancelContinuousJobs() + { + using (TestSystemClock.Install()) + { + var job = new HelloWorldJob(Log); + var timeoutCancellationTokenSource = new CancellationTokenSource(100); + await job.RunContinuousAsync(TimeSpan.FromSeconds(1), 5, timeoutCancellationTokenSource.Token); - HelloWorldJob.GlobalRunCount = 0; - using (var timeoutCancellationTokenSource = new CancellationTokenSource(50000)) - { - await new JobRunner(job, Log, instanceCount: 5, iterationLimit: 100).RunAsync(timeoutCancellationTokenSource.Token); - } + Assert.Equal(1, job.RunCount); - Assert.Equal(500, HelloWorldJob.GlobalRunCount); + timeoutCancellationTokenSource = new CancellationTokenSource(500); + var runnerTask = new JobRunner(job, Log, instanceCount: 5, iterationLimit: 10000, interval: TimeSpan.FromMilliseconds(1)).RunAsync(timeoutCancellationTokenSource.Token); + await SystemClock.SleepAsync(TimeSpan.FromSeconds(1)); + await runnerTask; } + } - [Fact] - public async Task CanCancelContinuousJobs() - { - using (TestSystemClock.Install()) - { - var job = new HelloWorldJob(Log); - var timeoutCancellationTokenSource = new CancellationTokenSource(100); - await job.RunContinuousAsync(TimeSpan.FromSeconds(1), 5, timeoutCancellationTokenSource.Token); - - Assert.Equal(1, job.RunCount); - - timeoutCancellationTokenSource = new CancellationTokenSource(500); - var runnerTask = new JobRunner(job, Log, instanceCount: 5, iterationLimit: 10000, interval: TimeSpan.FromMilliseconds(1)).RunAsync(timeoutCancellationTokenSource.Token); - await SystemClock.SleepAsync(TimeSpan.FromSeconds(1)); - await runnerTask; - } - } + [RetryFact] + public async Task CanRunJobsWithLocks() + { + var job = new WithLockingJob(Log); + Assert.Equal(0, job.RunCount); + await job.RunAsync(); + Assert.Equal(1, job.RunCount); - [RetryFact] - public async Task CanRunJobsWithLocks() - { - var job = new WithLockingJob(Log); - Assert.Equal(0, job.RunCount); - await job.RunAsync(); - Assert.Equal(1, job.RunCount); + await job.RunContinuousAsync(iterationLimit: 2); + Assert.Equal(3, job.RunCount); - await job.RunContinuousAsync(iterationLimit: 2); - Assert.Equal(3, job.RunCount); + await Run.InParallelAsync(2, i => job.RunAsync()); + Assert.Equal(4, job.RunCount); + } - await Run.InParallelAsync(2, i => job.RunAsync()); - Assert.Equal(4, job.RunCount); - } + [Fact] + public async Task CanRunThrottledJobs() + { + using var client = new InMemoryCacheClient(new InMemoryCacheClientOptions { LoggerFactory = Log }); + var jobs = new List(new[] { new ThrottledJob(client, Log), new ThrottledJob(client, Log), new ThrottledJob(client, Log) }); - [Fact] - public async Task CanRunThrottledJobs() + var sw = Stopwatch.StartNew(); + using (var timeoutCancellationTokenSource = new CancellationTokenSource(1000)) { - using var client = new InMemoryCacheClient(new InMemoryCacheClientOptions { LoggerFactory = Log }); - var jobs = new List(new[] { new ThrottledJob(client, Log), new ThrottledJob(client, Log), new ThrottledJob(client, Log) }); - - var sw = Stopwatch.StartNew(); - using (var timeoutCancellationTokenSource = new CancellationTokenSource(1000)) - { - await Task.WhenAll(jobs.Select(job => job.RunContinuousAsync(TimeSpan.FromMilliseconds(1), cancellationToken: timeoutCancellationTokenSource.Token))); - } - sw.Stop(); - Assert.InRange(jobs.Sum(j => j.RunCount), 4, 14); - _logger.LogInformation(jobs.Sum(j => j.RunCount).ToString()); - Assert.InRange(sw.ElapsedMilliseconds, 20, 1500); + await Task.WhenAll(jobs.Select(job => job.RunContinuousAsync(TimeSpan.FromMilliseconds(1), cancellationToken: timeoutCancellationTokenSource.Token))); } + sw.Stop(); + Assert.InRange(jobs.Sum(j => j.RunCount), 4, 14); + _logger.LogInformation(jobs.Sum(j => j.RunCount).ToString()); + Assert.InRange(sw.ElapsedMilliseconds, 20, 1500); + } - [Fact] - public async Task CanRunJobsWithInterval() + [Fact] + public async Task CanRunJobsWithInterval() + { + using (TestSystemClock.Install()) { - using (TestSystemClock.Install()) - { - var time = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + var time = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); - TestSystemClock.SetFrozenTime(time); - TestSystemClock.UseFakeSleep(); + TestSystemClock.SetFrozenTime(time); + TestSystemClock.UseFakeSleep(); - var job = new HelloWorldJob(Log); - var interval = TimeSpan.FromHours(.75); + var job = new HelloWorldJob(Log); + var interval = TimeSpan.FromHours(.75); - await job.RunContinuousAsync(iterationLimit: 2, interval: interval); + await job.RunContinuousAsync(iterationLimit: 2, interval: interval); - Assert.Equal(2, job.RunCount); - Assert.Equal(interval, (SystemClock.UtcNow - time)); - } + Assert.Equal(2, job.RunCount); + Assert.Equal(interval, (SystemClock.UtcNow - time)); } + } - [Fact] - public async Task CanRunJobsWithIntervalBetweenFailingJob() + [Fact] + public async Task CanRunJobsWithIntervalBetweenFailingJob() + { + using (TestSystemClock.Install()) { - using (TestSystemClock.Install()) - { - var time = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + var time = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); - TestSystemClock.SetFrozenTime(time); - TestSystemClock.UseFakeSleep(); + TestSystemClock.SetFrozenTime(time); + TestSystemClock.UseFakeSleep(); - var job = new FailingJob(Log); - var interval = TimeSpan.FromHours(.75); + var job = new FailingJob(Log); + var interval = TimeSpan.FromHours(.75); - await job.RunContinuousAsync(iterationLimit: 2, interval: interval); + await job.RunContinuousAsync(iterationLimit: 2, interval: interval); - Assert.Equal(2, job.RunCount); - Assert.Equal(interval, (SystemClock.UtcNow - time)); - } + Assert.Equal(2, job.RunCount); + Assert.Equal(interval, (SystemClock.UtcNow - time)); } + } - [Fact(Skip = "Meant to be run manually.")] - public async Task JobLoopPerf() - { - const int iterations = 10000; - - var metrics = new InMemoryMetricsClient(new InMemoryMetricsClientOptions { LoggerFactory = Log }); - var job = new SampleJob(metrics, Log); - var sw = Stopwatch.StartNew(); - await job.RunContinuousAsync(null, iterations); - sw.Stop(); - await metrics.FlushAsync(); - _logger.LogTrace((await metrics.GetCounterStatsAsync("runs")).ToString()); - _logger.LogTrace((await metrics.GetCounterStatsAsync("errors")).ToString()); - _logger.LogTrace((await metrics.GetCounterStatsAsync("failed")).ToString()); - _logger.LogTrace((await metrics.GetCounterStatsAsync("completed")).ToString()); - } + [Fact(Skip = "Meant to be run manually.")] + public async Task JobLoopPerf() + { + const int iterations = 10000; + + var metrics = new InMemoryMetricsClient(new InMemoryMetricsClientOptions { LoggerFactory = Log }); + var job = new SampleJob(metrics, Log); + var sw = Stopwatch.StartNew(); + await job.RunContinuousAsync(null, iterations); + sw.Stop(); + await metrics.FlushAsync(); + _logger.LogTrace((await metrics.GetCounterStatsAsync("runs")).ToString()); + _logger.LogTrace((await metrics.GetCounterStatsAsync("errors")).ToString()); + _logger.LogTrace((await metrics.GetCounterStatsAsync("failed")).ToString()); + _logger.LogTrace((await metrics.GetCounterStatsAsync("completed")).ToString()); } } diff --git a/tests/Foundatio.Tests/Jobs/WorkItemJobTests.cs b/tests/Foundatio.Tests/Jobs/WorkItemJobTests.cs index 4d5671206..575f76f6e 100644 --- a/tests/Foundatio.Tests/Jobs/WorkItemJobTests.cs +++ b/tests/Foundatio.Tests/Jobs/WorkItemJobTests.cs @@ -17,289 +17,288 @@ using Xunit; using Xunit.Abstractions; -namespace Foundatio.Tests.Jobs +namespace Foundatio.Tests.Jobs; + +public class WorkItemJobTests : TestWithLoggingBase { - public class WorkItemJobTests : TestWithLoggingBase + public WorkItemJobTests(ITestOutputHelper output) : base(output) { } + + [Fact] + public async Task CanRunWorkItem() { - public WorkItemJobTests(ITestOutputHelper output) : base(output) { } + using var queue = new InMemoryQueue(o => o.LoggerFactory(Log)); + using var messageBus = new InMemoryMessageBus(o => o.LoggerFactory(Log)); + var handlerRegistry = new WorkItemHandlers(); + var job = new WorkItemJob(queue, messageBus, handlerRegistry, Log); - [Fact] - public async Task CanRunWorkItem() + handlerRegistry.Register(async ctx => { - using var queue = new InMemoryQueue(o => o.LoggerFactory(Log)); - using var messageBus = new InMemoryMessageBus(o => o.LoggerFactory(Log)); - var handlerRegistry = new WorkItemHandlers(); - var job = new WorkItemJob(queue, messageBus, handlerRegistry, Log); + var jobData = ctx.GetData(); + Assert.Equal("Test", jobData.SomeData); - handlerRegistry.Register(async ctx => + for (int i = 0; i < 10; i++) { - var jobData = ctx.GetData(); - Assert.Equal("Test", jobData.SomeData); + await SystemClock.SleepAsync(100); + await ctx.ReportProgressAsync(10 * i); + } + }); - for (int i = 0; i < 10; i++) - { - await SystemClock.SleepAsync(100); - await ctx.ReportProgressAsync(10 * i); - } - }); + string jobId = await queue.EnqueueAsync(new MyWorkItem + { + SomeData = "Test" + }, true); - string jobId = await queue.EnqueueAsync(new MyWorkItem - { - SomeData = "Test" - }, true); + var countdown = new AsyncCountdownEvent(12); + await messageBus.SubscribeAsync(status => + { + _logger.LogInformation("Progress: {Progress}", status.Progress); + Assert.Equal(jobId, status.WorkItemId); + countdown.Signal(); + }); + + await job.RunAsync(); + await countdown.WaitAsync(TimeSpan.FromSeconds(2)); + Assert.Equal(0, countdown.CurrentCount); + } - var countdown = new AsyncCountdownEvent(12); - await messageBus.SubscribeAsync(status => - { - _logger.LogInformation("Progress: {Progress}", status.Progress); - Assert.Equal(jobId, status.WorkItemId); - countdown.Signal(); - }); - - await job.RunAsync(); - await countdown.WaitAsync(TimeSpan.FromSeconds(2)); - Assert.Equal(0, countdown.CurrentCount); - } + [Fact] + public async Task CanHandleMultipleWorkItemInstances() + { + const int workItemCount = 1000; + + using var metrics = new InMemoryMetricsClient(o => o.LoggerFactory(Log)); + using var queue = new InMemoryQueue(o => o.RetryDelay(TimeSpan.Zero).Retries(0).LoggerFactory(Log)); + queue.AttachBehavior(new MetricsQueueBehavior(metrics, loggerFactory: Log)); + using var messageBus = new InMemoryMessageBus(o => o.LoggerFactory(Log)); + var handlerRegistry = new WorkItemHandlers(); + var j1 = new WorkItemJob(queue, messageBus, handlerRegistry, Log); + var j2 = new WorkItemJob(queue, messageBus, handlerRegistry, Log); + var j3 = new WorkItemJob(queue, messageBus, handlerRegistry, Log); + int errors = 0; - [Fact] - public async Task CanHandleMultipleWorkItemInstances() + var jobIds = new ConcurrentDictionary(); + + handlerRegistry.Register(async ctx => { - const int workItemCount = 1000; + var jobData = ctx.GetData(); + Assert.Equal("Test", jobData.SomeData); - using var metrics = new InMemoryMetricsClient(o => o.LoggerFactory(Log)); - using var queue = new InMemoryQueue(o => o.RetryDelay(TimeSpan.Zero).Retries(0).LoggerFactory(Log)); - queue.AttachBehavior(new MetricsQueueBehavior(metrics, loggerFactory: Log)); - using var messageBus = new InMemoryMessageBus(o => o.LoggerFactory(Log)); - var handlerRegistry = new WorkItemHandlers(); - var j1 = new WorkItemJob(queue, messageBus, handlerRegistry, Log); - var j2 = new WorkItemJob(queue, messageBus, handlerRegistry, Log); - var j3 = new WorkItemJob(queue, messageBus, handlerRegistry, Log); - int errors = 0; + int jobWorkTotal = jobIds.AddOrUpdate(ctx.JobId, 1, (key, value) => value + 1); + if (jobData.Index % 100 == 0 && _logger.IsEnabled(LogLevel.Trace)) + _logger.LogTrace("Job {JobId} processing work item #: {JobWorkTotal}", ctx.JobId, jobWorkTotal); - var jobIds = new ConcurrentDictionary(); + for (int i = 0; i < 10; i++) + await ctx.ReportProgressAsync(10 * i); - handlerRegistry.Register(async ctx => + if (RandomData.GetBool(1)) { - var jobData = ctx.GetData(); - Assert.Equal("Test", jobData.SomeData); - - int jobWorkTotal = jobIds.AddOrUpdate(ctx.JobId, 1, (key, value) => value + 1); - if (jobData.Index % 100 == 0 && _logger.IsEnabled(LogLevel.Trace)) - _logger.LogTrace("Job {JobId} processing work item #: {JobWorkTotal}", ctx.JobId, jobWorkTotal); - - for (int i = 0; i < 10; i++) - await ctx.ReportProgressAsync(10 * i); - - if (RandomData.GetBool(1)) - { - Interlocked.Increment(ref errors); - throw new Exception("Boom!"); - } - }); - - for (int i = 0; i < workItemCount; i++) - await queue.EnqueueAsync(new MyWorkItem - { - SomeData = "Test", - Index = i - }, true); - - var completedItems = new List(); - object completedItemsLock = new(); - await messageBus.SubscribeAsync(status => - { - if (status.Progress == 100 && _logger.IsEnabled(LogLevel.Trace)) - _logger.LogTrace("Progress: {Progress}", status.Progress); - - if (status.Progress < 100) - return; - - lock (completedItemsLock) - completedItems.Add(status.WorkItemId); - }); - - var cancellationTokenSource = new CancellationTokenSource(10000); - var tasks = new List { - Task.Run(async () => { - await j1.RunUntilEmptyAsync(cancellationTokenSource.Token); - await cancellationTokenSource.CancelAsync(); - }, cancellationTokenSource.Token), - Task.Run(async () => { - await j2.RunUntilEmptyAsync(cancellationTokenSource.Token); - await cancellationTokenSource.CancelAsync(); - }, cancellationTokenSource.Token), - Task.Run(async () => { - await j3.RunUntilEmptyAsync(cancellationTokenSource.Token); - await cancellationTokenSource.CancelAsync(); - }, cancellationTokenSource.Token) - }; - - try - { - await Task.WhenAll(tasks); + Interlocked.Increment(ref errors); + throw new Exception("Boom!"); } - catch (OperationCanceledException ex) + }); + + for (int i = 0; i < workItemCount; i++) + await queue.EnqueueAsync(new MyWorkItem { - if (_logger.IsEnabled(LogLevel.Error)) - _logger.LogError(ex, "One or more tasks were cancelled: {Message}", ex.Message); - } + SomeData = "Test", + Index = i + }, true); - await SystemClock.SleepAsync(100); - if (_logger.IsEnabled(LogLevel.Information)) - _logger.LogInformation("Completed: {CompletedItems} Errors: {Errors}", completedItems.Count, errors); - Assert.Equal(workItemCount, completedItems.Count + errors); - Assert.Equal(3, jobIds.Count); - Assert.Equal(workItemCount, jobIds.Sum(kvp => kvp.Value)); + var completedItems = new List(); + object completedItemsLock = new(); + await messageBus.SubscribeAsync(status => + { + if (status.Progress == 100 && _logger.IsEnabled(LogLevel.Trace)) + _logger.LogTrace("Progress: {Progress}", status.Progress); + + if (status.Progress < 100) + return; + + lock (completedItemsLock) + completedItems.Add(status.WorkItemId); + }); + + var cancellationTokenSource = new CancellationTokenSource(10000); + var tasks = new List { + Task.Run(async () => { + await j1.RunUntilEmptyAsync(cancellationTokenSource.Token); + await cancellationTokenSource.CancelAsync(); + }, cancellationTokenSource.Token), + Task.Run(async () => { + await j2.RunUntilEmptyAsync(cancellationTokenSource.Token); + await cancellationTokenSource.CancelAsync(); + }, cancellationTokenSource.Token), + Task.Run(async () => { + await j3.RunUntilEmptyAsync(cancellationTokenSource.Token); + await cancellationTokenSource.CancelAsync(); + }, cancellationTokenSource.Token) + }; + + try + { + await Task.WhenAll(tasks); } - - [Fact] - public async Task CanRunWorkItemWithClassHandler() + catch (OperationCanceledException ex) { - using var queue = new InMemoryQueue(o => o.LoggerFactory(Log)); - using var messageBus = new InMemoryMessageBus(o => o.LoggerFactory(Log)); - var handlerRegistry = new WorkItemHandlers(); - var job = new WorkItemJob(queue, messageBus, handlerRegistry, Log); + if (_logger.IsEnabled(LogLevel.Error)) + _logger.LogError(ex, "One or more tasks were cancelled: {Message}", ex.Message); + } - handlerRegistry.Register(new MyWorkItemHandler(Log)); + await SystemClock.SleepAsync(100); + if (_logger.IsEnabled(LogLevel.Information)) + _logger.LogInformation("Completed: {CompletedItems} Errors: {Errors}", completedItems.Count, errors); + Assert.Equal(workItemCount, completedItems.Count + errors); + Assert.Equal(3, jobIds.Count); + Assert.Equal(workItemCount, jobIds.Sum(kvp => kvp.Value)); + } - string jobId = await queue.EnqueueAsync(new MyWorkItem - { - SomeData = "Test" - }, true); + [Fact] + public async Task CanRunWorkItemWithClassHandler() + { + using var queue = new InMemoryQueue(o => o.LoggerFactory(Log)); + using var messageBus = new InMemoryMessageBus(o => o.LoggerFactory(Log)); + var handlerRegistry = new WorkItemHandlers(); + var job = new WorkItemJob(queue, messageBus, handlerRegistry, Log); - var countdown = new AsyncCountdownEvent(11); - await messageBus.SubscribeAsync(status => - { - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Progress: {Progress}", status.Progress); - Assert.Equal(jobId, status.WorkItemId); - countdown.Signal(); - }); - - await job.RunUntilEmptyAsync(); - await countdown.WaitAsync(TimeSpan.FromSeconds(2)); - Assert.Equal(0, countdown.CurrentCount); - } + handlerRegistry.Register(new MyWorkItemHandler(Log)); - [Fact] - public async Task CanRunWorkItemWithDelegateHandler() + string jobId = await queue.EnqueueAsync(new MyWorkItem { - using var queue = new InMemoryQueue(o => o.LoggerFactory(Log)); - using var messageBus = new InMemoryMessageBus(o => o.LoggerFactory(Log)); - var handlerRegistry = new WorkItemHandlers(); - var job = new WorkItemJob(queue, messageBus, handlerRegistry, Log); + SomeData = "Test" + }, true); - handlerRegistry.Register(async ctx => - { - var jobData = ctx.GetData(); - Assert.Equal("Test", jobData.SomeData); + var countdown = new AsyncCountdownEvent(11); + await messageBus.SubscribeAsync(status => + { + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Progress: {Progress}", status.Progress); + Assert.Equal(jobId, status.WorkItemId); + countdown.Signal(); + }); + + await job.RunUntilEmptyAsync(); + await countdown.WaitAsync(TimeSpan.FromSeconds(2)); + Assert.Equal(0, countdown.CurrentCount); + } - for (int i = 1; i < 10; i++) - { - await SystemClock.SleepAsync(100); - await ctx.ReportProgressAsync(10 * i); - } - }, Log.CreateLogger("MyWorkItem")); + [Fact] + public async Task CanRunWorkItemWithDelegateHandler() + { + using var queue = new InMemoryQueue(o => o.LoggerFactory(Log)); + using var messageBus = new InMemoryMessageBus(o => o.LoggerFactory(Log)); + var handlerRegistry = new WorkItemHandlers(); + var job = new WorkItemJob(queue, messageBus, handlerRegistry, Log); - string jobId = await queue.EnqueueAsync(new MyWorkItem - { - SomeData = "Test" - }, true); + handlerRegistry.Register(async ctx => + { + var jobData = ctx.GetData(); + Assert.Equal("Test", jobData.SomeData); - var countdown = new AsyncCountdownEvent(11); - await messageBus.SubscribeAsync(status => + for (int i = 1; i < 10; i++) { - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Progress: {Progress}", status.Progress); - Assert.Equal(jobId, status.WorkItemId); - countdown.Signal(); - }); - - await job.RunUntilEmptyAsync(); - await countdown.WaitAsync(TimeSpan.FromSeconds(2)); - Assert.Equal(0, countdown.CurrentCount); - } + await SystemClock.SleepAsync(100); + await ctx.ReportProgressAsync(10 * i); + } + }, Log.CreateLogger("MyWorkItem")); - [Fact] - public async Task CanRunWorkItemJobUntilEmpty() + string jobId = await queue.EnqueueAsync(new MyWorkItem { - using var queue = new InMemoryQueue(o => o.LoggerFactory(Log)); - using var messageBus = new InMemoryMessageBus(o => o.LoggerFactory(Log)); - var handlerRegistry = new WorkItemHandlers(); - var job = new WorkItemJob(queue, messageBus, handlerRegistry, Log); + SomeData = "Test" + }, true); - handlerRegistry.Register(new MyWorkItemHandler(Log)); + var countdown = new AsyncCountdownEvent(11); + await messageBus.SubscribeAsync(status => + { + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Progress: {Progress}", status.Progress); + Assert.Equal(jobId, status.WorkItemId); + countdown.Signal(); + }); + + await job.RunUntilEmptyAsync(); + await countdown.WaitAsync(TimeSpan.FromSeconds(2)); + Assert.Equal(0, countdown.CurrentCount); + } - await queue.EnqueueAsync(new MyWorkItem - { - SomeData = "Test" - }, true); + [Fact] + public async Task CanRunWorkItemJobUntilEmpty() + { + using var queue = new InMemoryQueue(o => o.LoggerFactory(Log)); + using var messageBus = new InMemoryMessageBus(o => o.LoggerFactory(Log)); + var handlerRegistry = new WorkItemHandlers(); + var job = new WorkItemJob(queue, messageBus, handlerRegistry, Log); - await queue.EnqueueAsync(new MyWorkItem - { - SomeData = "Test" - }, true); + handlerRegistry.Register(new MyWorkItemHandler(Log)); - await job.RunUntilEmptyAsync(); - var stats = await queue.GetQueueStatsAsync(); - Assert.Equal(2, stats.Enqueued); - Assert.Equal(2, stats.Dequeued); - Assert.Equal(2, stats.Completed); - } + await queue.EnqueueAsync(new MyWorkItem + { + SomeData = "Test" + }, true); - [Fact] - public async Task CanRunBadWorkItem() + await queue.EnqueueAsync(new MyWorkItem { - using var queue = new InMemoryQueue(o => o.RetryDelay(TimeSpan.FromMilliseconds(500)).LoggerFactory(Log)); - using var messageBus = new InMemoryMessageBus(o => o.LoggerFactory(Log)); - var handlerRegistry = new WorkItemHandlers(); - var job = new WorkItemJob(queue, messageBus, handlerRegistry, Log); + SomeData = "Test" + }, true); + + await job.RunUntilEmptyAsync(); + var stats = await queue.GetQueueStatsAsync(); + Assert.Equal(2, stats.Enqueued); + Assert.Equal(2, stats.Dequeued); + Assert.Equal(2, stats.Completed); + } - handlerRegistry.Register(ctx => - { - var jobData = ctx.GetData(); - Assert.Equal("Test", jobData.SomeData); - throw new Exception(); - }); + [Fact] + public async Task CanRunBadWorkItem() + { + using var queue = new InMemoryQueue(o => o.RetryDelay(TimeSpan.FromMilliseconds(500)).LoggerFactory(Log)); + using var messageBus = new InMemoryMessageBus(o => o.LoggerFactory(Log)); + var handlerRegistry = new WorkItemHandlers(); + var job = new WorkItemJob(queue, messageBus, handlerRegistry, Log); - string jobId = await queue.EnqueueAsync(new MyWorkItem - { - SomeData = "Test" - }, true); + handlerRegistry.Register(ctx => + { + var jobData = ctx.GetData(); + Assert.Equal("Test", jobData.SomeData); + throw new Exception(); + }); - var countdown = new AsyncCountdownEvent(2); - await messageBus.SubscribeAsync(status => - { - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Progress: {Progress}", status.Progress); - Assert.Equal(jobId, status.WorkItemId); - countdown.Signal(); - }); - - await job.RunUntilEmptyAsync(); - await countdown.WaitAsync(TimeSpan.FromSeconds(2)); - Assert.Equal(0, countdown.CurrentCount); - } - } + string jobId = await queue.EnqueueAsync(new MyWorkItem + { + SomeData = "Test" + }, true); - public class MyWorkItem - { - public string SomeData { get; set; } - public int Index { get; set; } + var countdown = new AsyncCountdownEvent(2); + await messageBus.SubscribeAsync(status => + { + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Progress: {Progress}", status.Progress); + Assert.Equal(jobId, status.WorkItemId); + countdown.Signal(); + }); + + await job.RunUntilEmptyAsync(); + await countdown.WaitAsync(TimeSpan.FromSeconds(2)); + Assert.Equal(0, countdown.CurrentCount); } +} + +public class MyWorkItem +{ + public string SomeData { get; set; } + public int Index { get; set; } +} + +public class MyWorkItemHandler : WorkItemHandlerBase +{ + public MyWorkItemHandler(ILoggerFactory loggerFactory = null) : base(loggerFactory) { } - public class MyWorkItemHandler : WorkItemHandlerBase + public override async Task HandleItemAsync(WorkItemContext context) { - public MyWorkItemHandler(ILoggerFactory loggerFactory = null) : base(loggerFactory) { } + var jobData = context.GetData(); + Assert.Equal("Test", jobData.SomeData); - public override async Task HandleItemAsync(WorkItemContext context) + for (int i = 1; i < 10; i++) { - var jobData = context.GetData(); - Assert.Equal("Test", jobData.SomeData); - - for (int i = 1; i < 10; i++) - { - await SystemClock.SleepAsync(100); - await context.ReportProgressAsync(10 * i); - } + await SystemClock.SleepAsync(100); + await context.ReportProgressAsync(10 * i); } } } diff --git a/tests/Foundatio.Tests/Locks/InMemoryLockTests.cs b/tests/Foundatio.Tests/Locks/InMemoryLockTests.cs index d0ffe1e4d..af162c2a7 100644 --- a/tests/Foundatio.Tests/Locks/InMemoryLockTests.cs +++ b/tests/Foundatio.Tests/Locks/InMemoryLockTests.cs @@ -9,87 +9,86 @@ using Xunit; using Xunit.Abstractions; -namespace Foundatio.Tests.Locks +namespace Foundatio.Tests.Locks; + +public class InMemoryLockTests : LockTestBase, IDisposable { - public class InMemoryLockTests : LockTestBase, IDisposable - { - private readonly ICacheClient _cache; - private readonly IMessageBus _messageBus; + private readonly ICacheClient _cache; + private readonly IMessageBus _messageBus; - public InMemoryLockTests(ITestOutputHelper output) : base(output) - { - _cache = new InMemoryCacheClient(o => o.LoggerFactory(Log)); - _messageBus = new InMemoryMessageBus(o => o.LoggerFactory(Log)); - } + public InMemoryLockTests(ITestOutputHelper output) : base(output) + { + _cache = new InMemoryCacheClient(o => o.LoggerFactory(Log)); + _messageBus = new InMemoryMessageBus(o => o.LoggerFactory(Log)); + } - protected override ILockProvider GetThrottlingLockProvider(int maxHits, TimeSpan period) - { - return new ThrottlingLockProvider(_cache, maxHits, period, Log); - } + protected override ILockProvider GetThrottlingLockProvider(int maxHits, TimeSpan period) + { + return new ThrottlingLockProvider(_cache, maxHits, period, Log); + } - protected override ILockProvider GetLockProvider() - { - return new CacheLockProvider(_cache, _messageBus, Log); - } + protected override ILockProvider GetLockProvider() + { + return new CacheLockProvider(_cache, _messageBus, Log); + } - [Fact] - public override Task CanAcquireAndReleaseLockAsync() + [Fact] + public override Task CanAcquireAndReleaseLockAsync() + { + using (TestSystemClock.Install()) { - using (TestSystemClock.Install()) - { - return base.CanAcquireAndReleaseLockAsync(); - } + return base.CanAcquireAndReleaseLockAsync(); } + } - [Fact] - public override Task LockWillTimeoutAsync() - { - return base.LockWillTimeoutAsync(); - } + [Fact] + public override Task LockWillTimeoutAsync() + { + return base.LockWillTimeoutAsync(); + } - [Fact] - public override Task LockOneAtATimeAsync() - { - return base.LockOneAtATimeAsync(); - } + [Fact] + public override Task LockOneAtATimeAsync() + { + return base.LockOneAtATimeAsync(); + } - [Fact] - public override Task CanAcquireMultipleResources() - { - return base.CanAcquireMultipleResources(); - } + [Fact] + public override Task CanAcquireMultipleResources() + { + return base.CanAcquireMultipleResources(); + } - [Fact] - public override Task CanAcquireLocksInParallel() - { - return base.CanAcquireLocksInParallel(); - } + [Fact] + public override Task CanAcquireLocksInParallel() + { + return base.CanAcquireLocksInParallel(); + } - [Fact] - public override Task CanAcquireMultipleScopedResources() - { - return base.CanAcquireMultipleScopedResources(); - } + [Fact] + public override Task CanAcquireMultipleScopedResources() + { + return base.CanAcquireMultipleScopedResources(); + } - [RetryFact] - public override Task WillThrottleCallsAsync() - { - Log.SetLogLevel(LogLevel.Trace); - Log.SetLogLevel(LogLevel.Trace); + [RetryFact] + public override Task WillThrottleCallsAsync() + { + Log.SetLogLevel(LogLevel.Trace); + Log.SetLogLevel(LogLevel.Trace); - return base.WillThrottleCallsAsync(); - } + return base.WillThrottleCallsAsync(); + } - [Fact] - public override Task CanReleaseLockMultipleTimes() - { - return base.CanReleaseLockMultipleTimes(); - } + [Fact] + public override Task CanReleaseLockMultipleTimes() + { + return base.CanReleaseLockMultipleTimes(); + } - public void Dispose() - { - _cache.Dispose(); - _messageBus.Dispose(); - } + public void Dispose() + { + _cache.Dispose(); + _messageBus.Dispose(); } } diff --git a/tests/Foundatio.Tests/Messaging/InMemoryMessageBusTests.cs b/tests/Foundatio.Tests/Messaging/InMemoryMessageBusTests.cs index 0cff44adf..320e0c80d 100644 --- a/tests/Foundatio.Tests/Messaging/InMemoryMessageBusTests.cs +++ b/tests/Foundatio.Tests/Messaging/InMemoryMessageBusTests.cs @@ -4,155 +4,154 @@ using Xunit; using Xunit.Abstractions; -namespace Foundatio.Tests.Messaging +namespace Foundatio.Tests.Messaging; + +public class InMemoryMessageBusTests : MessageBusTestBase, IDisposable { - public class InMemoryMessageBusTests : MessageBusTestBase, IDisposable - { - private IMessageBus _messageBus; + private IMessageBus _messageBus; + + public InMemoryMessageBusTests(ITestOutputHelper output) : base(output) { } - public InMemoryMessageBusTests(ITestOutputHelper output) : base(output) { } + protected override IMessageBus GetMessageBus(Func config = null) + { + if (_messageBus != null) + return _messageBus; - protected override IMessageBus GetMessageBus(Func config = null) + _messageBus = new InMemoryMessageBus(o => { - if (_messageBus != null) - return _messageBus; + o.LoggerFactory(Log); + if (config != null) + config(o.Target); - _messageBus = new InMemoryMessageBus(o => - { - o.LoggerFactory(Log); - if (config != null) - config(o.Target); + return o; + }); + return _messageBus; + } - return o; - }); - return _messageBus; - } + [Fact] + public override Task CanUseMessageOptionsAsync() + { + return base.CanUseMessageOptionsAsync(); + } - [Fact] - public override Task CanUseMessageOptionsAsync() - { - return base.CanUseMessageOptionsAsync(); - } + [Fact] + public async Task CanCheckMessageCounts() + { + var messageBus = new InMemoryMessageBus(o => o.LoggerFactory(Log)); + await messageBus.PublishAsync(new SimpleMessageA + { + Data = "Hello" + }); + Assert.Equal(1, messageBus.MessagesSent); + Assert.Equal(1, messageBus.GetMessagesSent()); + Assert.Equal(0, messageBus.GetMessagesSent()); + } - [Fact] - public async Task CanCheckMessageCounts() - { - var messageBus = new InMemoryMessageBus(o => o.LoggerFactory(Log)); - await messageBus.PublishAsync(new SimpleMessageA - { - Data = "Hello" - }); - Assert.Equal(1, messageBus.MessagesSent); - Assert.Equal(1, messageBus.GetMessagesSent()); - Assert.Equal(0, messageBus.GetMessagesSent()); - } - - [Fact] - public override Task CanSendMessageAsync() - { - return base.CanSendMessageAsync(); - } + [Fact] + public override Task CanSendMessageAsync() + { + return base.CanSendMessageAsync(); + } - [Fact] - public override Task CanHandleNullMessageAsync() - { - return base.CanHandleNullMessageAsync(); - } + [Fact] + public override Task CanHandleNullMessageAsync() + { + return base.CanHandleNullMessageAsync(); + } - [Fact] - public override Task CanSendDerivedMessageAsync() - { - return base.CanSendDerivedMessageAsync(); - } + [Fact] + public override Task CanSendDerivedMessageAsync() + { + return base.CanSendDerivedMessageAsync(); + } - [Fact] - public override Task CanSendMappedMessageAsync() - { - return base.CanSendMappedMessageAsync(); - } + [Fact] + public override Task CanSendMappedMessageAsync() + { + return base.CanSendMappedMessageAsync(); + } - [Fact] - public override Task CanSendDelayedMessageAsync() - { - return base.CanSendDelayedMessageAsync(); - } + [Fact] + public override Task CanSendDelayedMessageAsync() + { + return base.CanSendDelayedMessageAsync(); + } - [Fact] - public override Task CanSubscribeConcurrentlyAsync() - { - return base.CanSubscribeConcurrentlyAsync(); - } + [Fact] + public override Task CanSubscribeConcurrentlyAsync() + { + return base.CanSubscribeConcurrentlyAsync(); + } - [Fact] - public override Task CanReceiveMessagesConcurrentlyAsync() - { - return base.CanReceiveMessagesConcurrentlyAsync(); - } + [Fact] + public override Task CanReceiveMessagesConcurrentlyAsync() + { + return base.CanReceiveMessagesConcurrentlyAsync(); + } - [Fact] - public override Task CanSendMessageToMultipleSubscribersAsync() - { - return base.CanSendMessageToMultipleSubscribersAsync(); - } + [Fact] + public override Task CanSendMessageToMultipleSubscribersAsync() + { + return base.CanSendMessageToMultipleSubscribersAsync(); + } - [Fact] - public override Task CanTolerateSubscriberFailureAsync() - { - return base.CanTolerateSubscriberFailureAsync(); - } + [Fact] + public override Task CanTolerateSubscriberFailureAsync() + { + return base.CanTolerateSubscriberFailureAsync(); + } - [Fact] - public override Task WillOnlyReceiveSubscribedMessageTypeAsync() - { - return base.WillOnlyReceiveSubscribedMessageTypeAsync(); - } + [Fact] + public override Task WillOnlyReceiveSubscribedMessageTypeAsync() + { + return base.WillOnlyReceiveSubscribedMessageTypeAsync(); + } - [Fact] - public override Task WillReceiveDerivedMessageTypesAsync() - { - return base.WillReceiveDerivedMessageTypesAsync(); - } + [Fact] + public override Task WillReceiveDerivedMessageTypesAsync() + { + return base.WillReceiveDerivedMessageTypesAsync(); + } - [Fact] - public override Task CanSubscribeToAllMessageTypesAsync() - { - return base.CanSubscribeToAllMessageTypesAsync(); - } + [Fact] + public override Task CanSubscribeToAllMessageTypesAsync() + { + return base.CanSubscribeToAllMessageTypesAsync(); + } - [Fact] - public override Task CanSubscribeToRawMessagesAsync() - { - return base.CanSubscribeToRawMessagesAsync(); - } + [Fact] + public override Task CanSubscribeToRawMessagesAsync() + { + return base.CanSubscribeToRawMessagesAsync(); + } - [Fact] - public override Task CanCancelSubscriptionAsync() - { - return base.CanCancelSubscriptionAsync(); - } + [Fact] + public override Task CanCancelSubscriptionAsync() + { + return base.CanCancelSubscriptionAsync(); + } - [Fact] - public override Task WontKeepMessagesWithNoSubscribersAsync() - { - return base.WontKeepMessagesWithNoSubscribersAsync(); - } + [Fact] + public override Task WontKeepMessagesWithNoSubscribersAsync() + { + return base.WontKeepMessagesWithNoSubscribersAsync(); + } - [Fact] - public override Task CanReceiveFromMultipleSubscribersAsync() - { - return base.CanReceiveFromMultipleSubscribersAsync(); - } + [Fact] + public override Task CanReceiveFromMultipleSubscribersAsync() + { + return base.CanReceiveFromMultipleSubscribersAsync(); + } - [Fact] - public override void CanDisposeWithNoSubscribersOrPublishers() - { - base.CanDisposeWithNoSubscribersOrPublishers(); - } + [Fact] + public override void CanDisposeWithNoSubscribersOrPublishers() + { + base.CanDisposeWithNoSubscribersOrPublishers(); + } - public void Dispose() - { - _messageBus?.Dispose(); - _messageBus = null; - } + public void Dispose() + { + _messageBus?.Dispose(); + _messageBus = null; } } diff --git a/tests/Foundatio.Tests/Metrics/DiagnosticsMetricsTests.cs b/tests/Foundatio.Tests/Metrics/DiagnosticsMetricsTests.cs index 5bd18b6dc..b0f9012d9 100644 --- a/tests/Foundatio.Tests/Metrics/DiagnosticsMetricsTests.cs +++ b/tests/Foundatio.Tests/Metrics/DiagnosticsMetricsTests.cs @@ -8,107 +8,106 @@ using Xunit; using Xunit.Abstractions; -namespace Foundatio.Tests.Metrics +namespace Foundatio.Tests.Metrics; + +public class DiagnosticsMetricsTests : TestWithLoggingBase, IDisposable { - public class DiagnosticsMetricsTests : TestWithLoggingBase, IDisposable + private readonly DiagnosticsMetricsClient _client; + + public DiagnosticsMetricsTests(ITestOutputHelper output) : base(output) { - private readonly DiagnosticsMetricsClient _client; + Log.MinimumLevel = LogLevel.Trace; + _client = new DiagnosticsMetricsClient(o => o.MeterName("Test")); + } - public DiagnosticsMetricsTests(ITestOutputHelper output) : base(output) - { - Log.MinimumLevel = LogLevel.Trace; - _client = new DiagnosticsMetricsClient(o => o.MeterName("Test")); - } + [Fact] + public void Counter() + { + using var metricsCollector = new DiagnosticsMetricsCollector("Test", _logger); - [Fact] - public void Counter() - { - using var metricsCollector = new DiagnosticsMetricsCollector("Test", _logger); + _client.Counter("counter"); - _client.Counter("counter"); + Assert.Single(metricsCollector.GetMeasurements()); + Assert.Equal("counter", metricsCollector.GetMeasurements().Single().Name); + Assert.Equal(1, metricsCollector.GetMeasurements().Single().Value); + } - Assert.Single(metricsCollector.GetMeasurements()); - Assert.Equal("counter", metricsCollector.GetMeasurements().Single().Name); - Assert.Equal(1, metricsCollector.GetMeasurements().Single().Value); - } + [Fact] + public void CounterWithValue() + { + using var metricsCollector = new DiagnosticsMetricsCollector("Test", _logger); - [Fact] - public void CounterWithValue() - { - using var metricsCollector = new DiagnosticsMetricsCollector("Test", _logger); - - _client.Counter("counter", 5); - _client.Counter("counter", 3); - - Assert.Equal(2, metricsCollector.GetMeasurements().Count); - Assert.All(metricsCollector.GetMeasurements(), m => - { - Assert.Equal("counter", m.Name); - }); - Assert.Equal(8, metricsCollector.GetSum("counter")); - Assert.Equal(2, metricsCollector.GetCount("counter")); - } - - [Fact] - public void Gauge() + _client.Counter("counter", 5); + _client.Counter("counter", 3); + + Assert.Equal(2, metricsCollector.GetMeasurements().Count); + Assert.All(metricsCollector.GetMeasurements(), m => { - using var metricsCollector = new DiagnosticsMetricsCollector("Test", _logger); + Assert.Equal("counter", m.Name); + }); + Assert.Equal(8, metricsCollector.GetSum("counter")); + Assert.Equal(2, metricsCollector.GetCount("counter")); + } - _client.Gauge("gauge", 1.1); + [Fact] + public void Gauge() + { + using var metricsCollector = new DiagnosticsMetricsCollector("Test", _logger); - metricsCollector.RecordObservableInstruments(); + _client.Gauge("gauge", 1.1); - Assert.Single(metricsCollector.GetMeasurements()); ; - Assert.Equal("gauge", metricsCollector.GetMeasurements().Single().Name); - Assert.Equal(1.1, metricsCollector.GetMeasurements().Single().Value); - } + metricsCollector.RecordObservableInstruments(); - [Fact] - public void Timer() - { - using var metricsCollector = new DiagnosticsMetricsCollector("Test", _logger); - - _client.Timer("timer", 450); - _client.Timer("timer", 220); + Assert.Single(metricsCollector.GetMeasurements()); ; + Assert.Equal("gauge", metricsCollector.GetMeasurements().Single().Name); + Assert.Equal(1.1, metricsCollector.GetMeasurements().Single().Value); + } - Assert.Equal(670, metricsCollector.GetSum("timer")); - Assert.Equal(2, metricsCollector.GetCount("timer")); - } + [Fact] + public void Timer() + { + using var metricsCollector = new DiagnosticsMetricsCollector("Test", _logger); - [Fact] - public async Task CanWaitForCounter() - { - using var metricsCollector = new DiagnosticsMetricsCollector("Test", _logger); + _client.Timer("timer", 450); + _client.Timer("timer", 220); - var success = await metricsCollector.WaitForCounterAsync("timer", () => - { - _client.Counter("timer", 1); - _client.Counter("timer", 2); - return Task.CompletedTask; - }, 3); + Assert.Equal(670, metricsCollector.GetSum("timer")); + Assert.Equal(2, metricsCollector.GetCount("timer")); + } - Assert.True(success); - } + [Fact] + public async Task CanWaitForCounter() + { + using var metricsCollector = new DiagnosticsMetricsCollector("Test", _logger); - [Fact] - public async Task CanTimeoutWaitingForCounter() + var success = await metricsCollector.WaitForCounterAsync("timer", () => { - using var metricsCollector = new DiagnosticsMetricsCollector("Test", _logger); + _client.Counter("timer", 1); + _client.Counter("timer", 2); + return Task.CompletedTask; + }, 3); - var success = await metricsCollector.WaitForCounterAsync("timer", () => - { - _client.Counter("timer", 1); - _client.Counter("timer", 2); - return Task.CompletedTask; - }, 4, new CancellationTokenSource(TimeSpan.FromSeconds(1)).Token); + Assert.True(success); + } - Assert.False(success); - } + [Fact] + public async Task CanTimeoutWaitingForCounter() + { + using var metricsCollector = new DiagnosticsMetricsCollector("Test", _logger); - public void Dispose() + var success = await metricsCollector.WaitForCounterAsync("timer", () => { - _client.Dispose(); - GC.SuppressFinalize(this); - } + _client.Counter("timer", 1); + _client.Counter("timer", 2); + return Task.CompletedTask; + }, 4, new CancellationTokenSource(TimeSpan.FromSeconds(1)).Token); + + Assert.False(success); + } + + public void Dispose() + { + _client.Dispose(); + GC.SuppressFinalize(this); } } diff --git a/tests/Foundatio.Tests/Metrics/InMemoryMetricsTests.cs b/tests/Foundatio.Tests/Metrics/InMemoryMetricsTests.cs index 2dd9c239a..46ac16509 100644 --- a/tests/Foundatio.Tests/Metrics/InMemoryMetricsTests.cs +++ b/tests/Foundatio.Tests/Metrics/InMemoryMetricsTests.cs @@ -6,54 +6,53 @@ using Xunit; using Xunit.Abstractions; -namespace Foundatio.Tests.Metrics +namespace Foundatio.Tests.Metrics; + +public class InMemoryMetricsTests : MetricsClientTestBase { - public class InMemoryMetricsTests : MetricsClientTestBase - { - public InMemoryMetricsTests(ITestOutputHelper output) : base(output) { } + public InMemoryMetricsTests(ITestOutputHelper output) : base(output) { } - public override IMetricsClient GetMetricsClient(bool buffered = false) - { - return new InMemoryMetricsClient(o => o.LoggerFactory(Log).Buffered(buffered)); - } + public override IMetricsClient GetMetricsClient(bool buffered = false) + { + return new InMemoryMetricsClient(o => o.LoggerFactory(Log).Buffered(buffered)); + } - [Fact] - public override Task CanSetGaugesAsync() - { - return base.CanSetGaugesAsync(); - } + [Fact] + public override Task CanSetGaugesAsync() + { + return base.CanSetGaugesAsync(); + } - [Fact] - public override Task CanIncrementCounterAsync() - { - return base.CanIncrementCounterAsync(); - } + [Fact] + public override Task CanIncrementCounterAsync() + { + return base.CanIncrementCounterAsync(); + } - [RetryFact] - public override Task CanWaitForCounterAsync() + [RetryFact] + public override Task CanWaitForCounterAsync() + { + using (TestSystemClock.Install()) { - using (TestSystemClock.Install()) - { - return base.CanWaitForCounterAsync(); - } + return base.CanWaitForCounterAsync(); } + } - [Fact] - public override Task CanGetBufferedQueueMetricsAsync() - { - return base.CanGetBufferedQueueMetricsAsync(); - } + [Fact] + public override Task CanGetBufferedQueueMetricsAsync() + { + return base.CanGetBufferedQueueMetricsAsync(); + } - [Fact] - public override Task CanIncrementBufferedCounterAsync() - { - return base.CanIncrementBufferedCounterAsync(); - } + [Fact] + public override Task CanIncrementBufferedCounterAsync() + { + return base.CanIncrementBufferedCounterAsync(); + } - [Fact] - public override Task CanSendBufferedMetricsAsync() - { - return base.CanSendBufferedMetricsAsync(); - } + [Fact] + public override Task CanSendBufferedMetricsAsync() + { + return base.CanSendBufferedMetricsAsync(); } } diff --git a/tests/Foundatio.Tests/Queue/InMemoryQueueTests.cs b/tests/Foundatio.Tests/Queue/InMemoryQueueTests.cs index 6ff8e57fb..969ece92e 100644 --- a/tests/Foundatio.Tests/Queue/InMemoryQueueTests.cs +++ b/tests/Foundatio.Tests/Queue/InMemoryQueueTests.cs @@ -8,428 +8,427 @@ using Xunit; using Xunit.Abstractions; -namespace Foundatio.Tests.Queue +namespace Foundatio.Tests.Queue; + +public class InMemoryQueueTests : QueueTestBase { - public class InMemoryQueueTests : QueueTestBase + private IQueue _queue; + + public InMemoryQueueTests(ITestOutputHelper output) : base(output) { } + + protected override IQueue GetQueue(int retries = 1, TimeSpan? workItemTimeout = null, TimeSpan? retryDelay = null, int[] retryMultipliers = null, int deadLetterMaxItems = 100, bool runQueueMaintenance = true) { - private IQueue _queue; + if (_queue == null) + _queue = new InMemoryQueue(o => o + .RetryDelay(retryDelay.GetValueOrDefault(TimeSpan.FromMinutes(1))) + .Retries(retries) + .RetryMultipliers(retryMultipliers ?? new[] { 1, 3, 5, 10 }) + .WorkItemTimeout(workItemTimeout.GetValueOrDefault(TimeSpan.FromMinutes(5))) + .LoggerFactory(Log)); + if (_logger.IsEnabled(LogLevel.Debug)) + _logger.LogDebug("Queue Id: {QueueId}", _queue.QueueId); + return _queue; + } - public InMemoryQueueTests(ITestOutputHelper output) : base(output) { } + protected override async Task CleanupQueueAsync(IQueue queue) + { + if (queue == null) + return; - protected override IQueue GetQueue(int retries = 1, TimeSpan? workItemTimeout = null, TimeSpan? retryDelay = null, int[] retryMultipliers = null, int deadLetterMaxItems = 100, bool runQueueMaintenance = true) + try { - if (_queue == null) - _queue = new InMemoryQueue(o => o - .RetryDelay(retryDelay.GetValueOrDefault(TimeSpan.FromMinutes(1))) - .Retries(retries) - .RetryMultipliers(retryMultipliers ?? new[] { 1, 3, 5, 10 }) - .WorkItemTimeout(workItemTimeout.GetValueOrDefault(TimeSpan.FromMinutes(5))) - .LoggerFactory(Log)); - if (_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("Queue Id: {QueueId}", _queue.QueueId); - return _queue; + await queue.DeleteQueueAsync(); } - - protected override async Task CleanupQueueAsync(IQueue queue) + catch (Exception ex) { - if (queue == null) - return; - - try - { - await queue.DeleteQueueAsync(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error cleaning up queue"); - } + _logger.LogError(ex, "Error cleaning up queue"); } + } - [Fact] - public async Task TestAsyncEvents() + [Fact] + public async Task TestAsyncEvents() + { + using var q = new InMemoryQueue(o => o.LoggerFactory(Log)); + var disposables = new List(5); + try { - using var q = new InMemoryQueue(o => o.LoggerFactory(Log)); - var disposables = new List(5); - try + disposables.Add(q.Enqueuing.AddHandler(async (sender, args) => { - disposables.Add(q.Enqueuing.AddHandler(async (sender, args) => - { - await SystemClock.SleepAsync(250); - _logger.LogInformation("First Enqueuing"); - })); - disposables.Add(q.Enqueuing.AddHandler(async (sender, args) => - { - await SystemClock.SleepAsync(250); - _logger.LogInformation("Second Enqueuing"); - })); - disposables.Add(q.Enqueued.AddHandler(async (sender, args) => - { - await SystemClock.SleepAsync(250); - _logger.LogInformation("First"); - })); - disposables.Add(q.Enqueued.AddHandler(async (sender, args) => - { - await SystemClock.SleepAsync(250); - _logger.LogInformation("Second"); - })); - - var sw = Stopwatch.StartNew(); - await q.EnqueueAsync(new SimpleWorkItem()); - sw.Stop(); - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Time {Elapsed:g}", sw.Elapsed); - - sw.Restart(); - await q.EnqueueAsync(new SimpleWorkItem()); - sw.Stop(); - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Time {Elapsed:g}", sw.Elapsed); - } - finally + await SystemClock.SleepAsync(250); + _logger.LogInformation("First Enqueuing"); + })); + disposables.Add(q.Enqueuing.AddHandler(async (sender, args) => { - foreach (var disposable in disposables) - disposable.Dispose(); - } - } + await SystemClock.SleepAsync(250); + _logger.LogInformation("Second Enqueuing"); + })); + disposables.Add(q.Enqueued.AddHandler(async (sender, args) => + { + await SystemClock.SleepAsync(250); + _logger.LogInformation("First"); + })); + disposables.Add(q.Enqueued.AddHandler(async (sender, args) => + { + await SystemClock.SleepAsync(250); + _logger.LogInformation("Second"); + })); - [Fact] - public async Task CanGetCompletedEntries() - { - using var q = new InMemoryQueue(o => o.LoggerFactory(Log).CompletedEntryRetentionLimit(10)); + var sw = Stopwatch.StartNew(); + await q.EnqueueAsync(new SimpleWorkItem()); + sw.Stop(); + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Time {Elapsed:g}", sw.Elapsed); + sw.Restart(); await q.EnqueueAsync(new SimpleWorkItem()); - Assert.Single(q.GetEntries()); - Assert.Empty(q.GetDequeuedEntries()); - Assert.Empty(q.GetCompletedEntries()); + sw.Stop(); + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Time {Elapsed:g}", sw.Elapsed); + } + finally + { + foreach (var disposable in disposables) + disposable.Dispose(); + } + } - var item = await q.DequeueAsync(); - Assert.Empty(q.GetEntries()); - Assert.Single(q.GetDequeuedEntries()); - Assert.Empty(q.GetCompletedEntries()); + [Fact] + public async Task CanGetCompletedEntries() + { + using var q = new InMemoryQueue(o => o.LoggerFactory(Log).CompletedEntryRetentionLimit(10)); - await item.CompleteAsync(); - Assert.Empty(q.GetEntries()); - Assert.Empty(q.GetDequeuedEntries()); - Assert.Single(q.GetCompletedEntries()); + await q.EnqueueAsync(new SimpleWorkItem()); + Assert.Single(q.GetEntries()); + Assert.Empty(q.GetDequeuedEntries()); + Assert.Empty(q.GetCompletedEntries()); - for (int i = 0; i < 100; i++) - { - await q.EnqueueAsync(new SimpleWorkItem()); - item = await q.DequeueAsync(); - await item.CompleteAsync(); - } + var item = await q.DequeueAsync(); + Assert.Empty(q.GetEntries()); + Assert.Single(q.GetDequeuedEntries()); + Assert.Empty(q.GetCompletedEntries()); - Assert.Empty(q.GetEntries()); - Assert.Empty(q.GetDequeuedEntries()); - Assert.Equal(10, q.GetCompletedEntries().Count); - } + await item.CompleteAsync(); + Assert.Empty(q.GetEntries()); + Assert.Empty(q.GetDequeuedEntries()); + Assert.Single(q.GetCompletedEntries()); - [Fact] - public override Task CanQueueAndDequeueWorkItemAsync() + for (int i = 0; i < 100; i++) { - return base.CanQueueAndDequeueWorkItemAsync(); + await q.EnqueueAsync(new SimpleWorkItem()); + item = await q.DequeueAsync(); + await item.CompleteAsync(); } - [Fact] - public override Task CanQueueAndDequeueWorkItemWithDelayAsync() - { - return base.CanQueueAndDequeueWorkItemWithDelayAsync(); - } + Assert.Empty(q.GetEntries()); + Assert.Empty(q.GetDequeuedEntries()); + Assert.Equal(10, q.GetCompletedEntries().Count); + } - [Fact] - public override Task CanUseQueueOptionsAsync() - { - return base.CanUseQueueOptionsAsync(); - } + [Fact] + public override Task CanQueueAndDequeueWorkItemAsync() + { + return base.CanQueueAndDequeueWorkItemAsync(); + } - [Fact] - public override Task CanDiscardDuplicateQueueEntriesAsync() - { - return base.CanDiscardDuplicateQueueEntriesAsync(); - } + [Fact] + public override Task CanQueueAndDequeueWorkItemWithDelayAsync() + { + return base.CanQueueAndDequeueWorkItemWithDelayAsync(); + } - [Fact] - public override Task CanDequeueWithCancelledTokenAsync() - { - return base.CanDequeueWithCancelledTokenAsync(); - } + [Fact] + public override Task CanUseQueueOptionsAsync() + { + return base.CanUseQueueOptionsAsync(); + } - [Fact] - public override Task CanDequeueEfficientlyAsync() - { - return base.CanDequeueEfficientlyAsync(); - } + [Fact] + public override Task CanDiscardDuplicateQueueEntriesAsync() + { + return base.CanDiscardDuplicateQueueEntriesAsync(); + } - [Fact] - public override Task CanResumeDequeueEfficientlyAsync() - { - return base.CanResumeDequeueEfficientlyAsync(); - } + [Fact] + public override Task CanDequeueWithCancelledTokenAsync() + { + return base.CanDequeueWithCancelledTokenAsync(); + } - [Fact] - public override Task CanQueueAndDequeueMultipleWorkItemsAsync() - { - return base.CanQueueAndDequeueMultipleWorkItemsAsync(); - } + [Fact] + public override Task CanDequeueEfficientlyAsync() + { + return base.CanDequeueEfficientlyAsync(); + } - [Fact] - public override Task WillNotWaitForItemAsync() - { - return base.WillNotWaitForItemAsync(); - } + [Fact] + public override Task CanResumeDequeueEfficientlyAsync() + { + return base.CanResumeDequeueEfficientlyAsync(); + } - [Fact] - public override Task WillWaitForItemAsync() - { - return base.WillWaitForItemAsync(); - } + [Fact] + public override Task CanQueueAndDequeueMultipleWorkItemsAsync() + { + return base.CanQueueAndDequeueMultipleWorkItemsAsync(); + } - [Fact] - public override Task DequeueWaitWillGetSignaledAsync() - { - return base.DequeueWaitWillGetSignaledAsync(); - } + [Fact] + public override Task WillNotWaitForItemAsync() + { + return base.WillNotWaitForItemAsync(); + } - [Fact] - public override Task CanUseQueueWorkerAsync() - { - return base.CanUseQueueWorkerAsync(); - } + [Fact] + public override Task WillWaitForItemAsync() + { + return base.WillWaitForItemAsync(); + } - [Fact] - public override Task CanHandleErrorInWorkerAsync() - { - return base.CanHandleErrorInWorkerAsync(); - } + [Fact] + public override Task DequeueWaitWillGetSignaledAsync() + { + return base.DequeueWaitWillGetSignaledAsync(); + } - [Fact] - public override Task WorkItemsWillTimeoutAsync() - { - using (TestSystemClock.Install()) - { - return base.WorkItemsWillTimeoutAsync(); - } - } + [Fact] + public override Task CanUseQueueWorkerAsync() + { + return base.CanUseQueueWorkerAsync(); + } - [Fact] - public override Task WorkItemsWillGetMovedToDeadletterAsync() - { - return base.WorkItemsWillGetMovedToDeadletterAsync(); - } + [Fact] + public override Task CanHandleErrorInWorkerAsync() + { + return base.CanHandleErrorInWorkerAsync(); + } - [Fact] - public override Task CanAutoCompleteWorkerAsync() + [Fact] + public override Task WorkItemsWillTimeoutAsync() + { + using (TestSystemClock.Install()) { - return base.CanAutoCompleteWorkerAsync(); + return base.WorkItemsWillTimeoutAsync(); } + } - [Fact] - public override Task CanHaveMultipleQueueInstancesAsync() - { - return base.CanHaveMultipleQueueInstancesAsync(); - } + [Fact] + public override Task WorkItemsWillGetMovedToDeadletterAsync() + { + return base.WorkItemsWillGetMovedToDeadletterAsync(); + } - [Fact] - public override Task CanDelayRetryAsync() - { - return base.CanDelayRetryAsync(); - } + [Fact] + public override Task CanAutoCompleteWorkerAsync() + { + return base.CanAutoCompleteWorkerAsync(); + } - [Fact] - public override Task CanRunWorkItemWithMetricsAsync() - { - return base.CanRunWorkItemWithMetricsAsync(); - } + [Fact] + public override Task CanHaveMultipleQueueInstancesAsync() + { + return base.CanHaveMultipleQueueInstancesAsync(); + } + + [Fact] + public override Task CanDelayRetryAsync() + { + return base.CanDelayRetryAsync(); + } + + [Fact] + public override Task CanRunWorkItemWithMetricsAsync() + { + return base.CanRunWorkItemWithMetricsAsync(); + } + + [Fact] + public override Task CanRenewLockAsync() + { + return base.CanRenewLockAsync(); + } + + [Fact] + public override Task CanAbandonQueueEntryOnceAsync() + { + return base.CanAbandonQueueEntryOnceAsync(); + } + + [Fact] + public override Task CanCompleteQueueEntryOnceAsync() + { + return base.CanCompleteQueueEntryOnceAsync(); + } + + [Fact] + public override Task CanDequeueWithLockingAsync() + { + return base.CanDequeueWithLockingAsync(); + } - [Fact] - public override Task CanRenewLockAsync() + [Fact] + public override Task CanHaveMultipleQueueInstancesWithLockingAsync() + { + return base.CanHaveMultipleQueueInstancesWithLockingAsync(); + } + + [Fact] + public override Task MaintainJobNotAbandon_NotWorkTimeOutEntry() + { + return base.MaintainJobNotAbandon_NotWorkTimeOutEntry(); + } + + [Fact] + public override Task VerifyRetryAttemptsAsync() + { + return base.VerifyRetryAttemptsAsync(); + } + + [Fact] + public override Task VerifyDelayedRetryAttemptsAsync() + { + return base.VerifyDelayedRetryAttemptsAsync(); + } + + [Fact] + public override Task CanHandleAutoAbandonInWorker() + { + Log.MinimumLevel = LogLevel.Trace; + return base.CanHandleAutoAbandonInWorker(); + } + + #region Issue239 + + class QueueEntry_Issue239 : IQueueEntry where T : class + { + IQueueEntry _queueEntry; + + public QueueEntry_Issue239(IQueueEntry queueEntry) { - return base.CanRenewLockAsync(); + _queueEntry = queueEntry; } - [Fact] - public override Task CanAbandonQueueEntryOnceAsync() + public T Value => _queueEntry.Value; + + public string Id => _queueEntry.Id; + + public string CorrelationId => _queueEntry.CorrelationId; + + public IDictionary Properties => _queueEntry.Properties; + + public Type EntryType => _queueEntry.EntryType; + + public bool IsCompleted => _queueEntry.IsCompleted; + + public bool IsAbandoned => _queueEntry.IsAbandoned; + + public int Attempts => _queueEntry.Attempts; + + public Task AbandonAsync() { - return base.CanAbandonQueueEntryOnceAsync(); + return _queueEntry.AbandonAsync(); } - [Fact] - public override Task CanCompleteQueueEntryOnceAsync() + public Task CompleteAsync() { - return base.CanCompleteQueueEntryOnceAsync(); + return _queueEntry.CompleteAsync(); } - [Fact] - public override Task CanDequeueWithLockingAsync() + public ValueTask DisposeAsync() { - return base.CanDequeueWithLockingAsync(); + return _queueEntry.DisposeAsync(); } - [Fact] - public override Task CanHaveMultipleQueueInstancesWithLockingAsync() + public object GetValue() { - return base.CanHaveMultipleQueueInstancesWithLockingAsync(); + return _queueEntry.GetValue(); } - [Fact] - public override Task MaintainJobNotAbandon_NotWorkTimeOutEntry() + public void MarkAbandoned() { - return base.MaintainJobNotAbandon_NotWorkTimeOutEntry(); + // we want to simulate timing of user complete call between the maintenance abandon call to _dequeued.TryRemove and entry.MarkAbandoned(); + Task.Delay(1500).Wait(); + + _queueEntry.MarkAbandoned(); } - [Fact] - public override Task VerifyRetryAttemptsAsync() + public void MarkCompleted() { - return base.VerifyRetryAttemptsAsync(); + _queueEntry.MarkCompleted(); } - [Fact] - public override Task VerifyDelayedRetryAttemptsAsync() + public Task RenewLockAsync() { - return base.VerifyDelayedRetryAttemptsAsync(); + return _queueEntry.RenewLockAsync(); } + } - [Fact] - public override Task CanHandleAutoAbandonInWorker() + class InMemoryQueue_Issue239 : InMemoryQueue where T : class + { + public override Task AbandonAsync(IQueueEntry entry) { - Log.MinimumLevel = LogLevel.Trace; - return base.CanHandleAutoAbandonInWorker(); + // delay first abandon from maintenance (simulate timing issues which may occur to demonstrate the problem) + return base.AbandonAsync(new QueueEntry_Issue239(entry)); } - #region Issue239 - - class QueueEntry_Issue239 : IQueueEntry where T : class + public InMemoryQueue_Issue239(ILoggerFactory loggerFactory) + : base(o => o + .RetryDelay(TimeSpan.FromMinutes(1)) + .Retries(1) + .RetryMultipliers(new[] { 1, 3, 5, 10 }) + .LoggerFactory(loggerFactory) + .WorkItemTimeout(TimeSpan.FromMilliseconds(100))) { - IQueueEntry _queueEntry; - - public QueueEntry_Issue239(IQueueEntry queueEntry) - { - _queueEntry = queueEntry; - } - - public T Value => _queueEntry.Value; - - public string Id => _queueEntry.Id; - - public string CorrelationId => _queueEntry.CorrelationId; - - public IDictionary Properties => _queueEntry.Properties; - - public Type EntryType => _queueEntry.EntryType; - - public bool IsCompleted => _queueEntry.IsCompleted; - - public bool IsAbandoned => _queueEntry.IsAbandoned; - - public int Attempts => _queueEntry.Attempts; - - public Task AbandonAsync() - { - return _queueEntry.AbandonAsync(); - } - - public Task CompleteAsync() - { - return _queueEntry.CompleteAsync(); - } - - public ValueTask DisposeAsync() - { - return _queueEntry.DisposeAsync(); - } - - public object GetValue() - { - return _queueEntry.GetValue(); - } - - public void MarkAbandoned() - { - // we want to simulate timing of user complete call between the maintenance abandon call to _dequeued.TryRemove and entry.MarkAbandoned(); - Task.Delay(1500).Wait(); + } + } - _queueEntry.MarkAbandoned(); - } + [Fact] + // this test reproduce an issue which cause worker task loop to crash and stop processing items when auto abandoned item is ultimately processed and user call complete on + // https://github.com/FoundatioFx/Foundatio/issues/239 + public virtual async Task CompleteOnAutoAbandonedHandledProperly_Issue239() + { + // create queue with short work item timeout so it will be auto abandoned + var queue = new InMemoryQueue_Issue239(Log); - public void MarkCompleted() - { - _queueEntry.MarkCompleted(); - } + // completion source to wait for CompleteAsync call before the assert + var taskCompletionSource = new TaskCompletionSource(); - public Task RenewLockAsync() + // start handling items + await queue.StartWorkingAsync(async (item) => + { + // we want to wait for maintainance to be performed and auto abandon our item, we don't have any way for waiting in IQueue so we'll settle for a delay + if (item.Value.Data == "Delay") { - return _queueEntry.RenewLockAsync(); + await Task.Delay(TimeSpan.FromSeconds(1)); } - } - class InMemoryQueue_Issue239 : InMemoryQueue where T : class - { - public override Task AbandonAsync(IQueueEntry entry) + try { - // delay first abandon from maintenance (simulate timing issues which may occur to demonstrate the problem) - return base.AbandonAsync(new QueueEntry_Issue239(entry)); + // call complete on the auto abandoned item + await item.CompleteAsync(); } - - public InMemoryQueue_Issue239(ILoggerFactory loggerFactory) - : base(o => o - .RetryDelay(TimeSpan.FromMinutes(1)) - .Retries(1) - .RetryMultipliers(new[] { 1, 3, 5, 10 }) - .LoggerFactory(loggerFactory) - .WorkItemTimeout(TimeSpan.FromMilliseconds(100))) + finally { + // completeAsync will currently throw an exception becuase item can not be removed from dequeued list because it was already removed due to auto abandon + // infrastructure handles user exception incorrectly + taskCompletionSource.SetResult(true); } - } + }); - [Fact] - // this test reproduce an issue which cause worker task loop to crash and stop processing items when auto abandoned item is ultimately processed and user call complete on - // https://github.com/FoundatioFx/Foundatio/issues/239 - public virtual async Task CompleteOnAutoAbandonedHandledProperly_Issue239() - { - // create queue with short work item timeout so it will be auto abandoned - var queue = new InMemoryQueue_Issue239(Log); + // enqueue item which will be processed after it's auto abandoned + await queue.EnqueueAsync(new SimpleWorkItem() { Data = "Delay" }); - // completion source to wait for CompleteAsync call before the assert - var taskCompletionSource = new TaskCompletionSource(); + // wait for taskCompletionSource.SetResult to be called or timeout after 1 second + bool timedout = (await Task.WhenAny(taskCompletionSource.Task, Task.Delay(TimeSpan.FromSeconds(2)))) != taskCompletionSource.Task; + Assert.False(timedout); - // start handling items - await queue.StartWorkingAsync(async (item) => - { - // we want to wait for maintainance to be performed and auto abandon our item, we don't have any way for waiting in IQueue so we'll settle for a delay - if (item.Value.Data == "Delay") - { - await Task.Delay(TimeSpan.FromSeconds(1)); - } - - try - { - // call complete on the auto abandoned item - await item.CompleteAsync(); - } - finally - { - // completeAsync will currently throw an exception becuase item can not be removed from dequeued list because it was already removed due to auto abandon - // infrastructure handles user exception incorrectly - taskCompletionSource.SetResult(true); - } - }); - - // enqueue item which will be processed after it's auto abandoned - await queue.EnqueueAsync(new SimpleWorkItem() { Data = "Delay" }); - - // wait for taskCompletionSource.SetResult to be called or timeout after 1 second - bool timedout = (await Task.WhenAny(taskCompletionSource.Task, Task.Delay(TimeSpan.FromSeconds(2)))) != taskCompletionSource.Task; - Assert.False(timedout); - - // enqueue another item and make sure it was handled (worker loop didn't crash) - taskCompletionSource = new TaskCompletionSource(); - await queue.EnqueueAsync(new SimpleWorkItem() { Data = "No Delay" }); - - // one option to fix this issue is surrounding the AbandonAsync call in StartWorkingImpl exception handler in inner try/catch block - timedout = (await Task.WhenAny(taskCompletionSource.Task, Task.Delay(TimeSpan.FromSeconds(2)))) != taskCompletionSource.Task; - Assert.False(timedout); + // enqueue another item and make sure it was handled (worker loop didn't crash) + taskCompletionSource = new TaskCompletionSource(); + await queue.EnqueueAsync(new SimpleWorkItem() { Data = "No Delay" }); - return; - } + // one option to fix this issue is surrounding the AbandonAsync call in StartWorkingImpl exception handler in inner try/catch block + timedout = (await Task.WhenAny(taskCompletionSource.Task, Task.Delay(TimeSpan.FromSeconds(2)))) != taskCompletionSource.Task; + Assert.False(timedout); - #endregion + return; } + + #endregion } diff --git a/tests/Foundatio.Tests/Serializer/CompressedMessagePackSerializerTests.cs b/tests/Foundatio.Tests/Serializer/CompressedMessagePackSerializerTests.cs index b221a21fb..f3a8b4bc8 100644 --- a/tests/Foundatio.Tests/Serializer/CompressedMessagePackSerializerTests.cs +++ b/tests/Foundatio.Tests/Serializer/CompressedMessagePackSerializerTests.cs @@ -5,52 +5,51 @@ using Xunit; using Xunit.Abstractions; -namespace Foundatio.Tests.Serializer +namespace Foundatio.Tests.Serializer; + +public class CompressedMessagePackSerializerTests : SerializerTestsBase { - public class CompressedMessagePackSerializerTests : SerializerTestsBase + public CompressedMessagePackSerializerTests(ITestOutputHelper output) : base(output) { } + + protected override ISerializer GetSerializer() + { + return new MessagePackSerializer(MessagePack.MessagePackSerializerOptions.Standard + .WithCompression(MessagePack.MessagePackCompression.Lz4Block) + .WithResolver(ContractlessStandardResolver.Instance)); + } + + [Fact] + public override void CanRoundTripBytes() + { + base.CanRoundTripBytes(); + } + + [Fact] + public override void CanRoundTripString() { - public CompressedMessagePackSerializerTests(ITestOutputHelper output) : base(output) { } - - protected override ISerializer GetSerializer() - { - return new MessagePackSerializer(MessagePack.MessagePackSerializerOptions.Standard - .WithCompression(MessagePack.MessagePackCompression.Lz4Block) - .WithResolver(ContractlessStandardResolver.Instance)); - } - - [Fact] - public override void CanRoundTripBytes() - { - base.CanRoundTripBytes(); - } - - [Fact] - public override void CanRoundTripString() - { - base.CanRoundTripString(); - } - - [Fact] - public override void CanHandlePrimitiveTypes() - { - base.CanHandlePrimitiveTypes(); - } - - [Fact(Skip = "Skip benchmarks for now")] - public virtual void Benchmark() - { - var summary = BenchmarkDotNet.Running.BenchmarkRunner.Run(); - _logger.LogInformation(summary.ToJson()); - } + base.CanRoundTripString(); } - public class CompressedMessagePackSerializerBenchmark : SerializerBenchmarkBase + [Fact] + public override void CanHandlePrimitiveTypes() + { + base.CanHandlePrimitiveTypes(); + } + + [Fact(Skip = "Skip benchmarks for now")] + public virtual void Benchmark() + { + var summary = BenchmarkDotNet.Running.BenchmarkRunner.Run(); + _logger.LogInformation(summary.ToJson()); + } +} + +public class CompressedMessagePackSerializerBenchmark : SerializerBenchmarkBase +{ + protected override ISerializer GetSerializer() { - protected override ISerializer GetSerializer() - { - return new MessagePackSerializer(MessagePack.MessagePackSerializerOptions.Standard - .WithCompression(MessagePack.MessagePackCompression.Lz4Block) - .WithResolver(ContractlessStandardResolver.Instance)); - } + return new MessagePackSerializer(MessagePack.MessagePackSerializerOptions.Standard + .WithCompression(MessagePack.MessagePackCompression.Lz4Block) + .WithResolver(ContractlessStandardResolver.Instance)); } } diff --git a/tests/Foundatio.Tests/Serializer/JsonNetSerializerTests.cs b/tests/Foundatio.Tests/Serializer/JsonNetSerializerTests.cs index e5a7a07b7..9f15d1a8b 100644 --- a/tests/Foundatio.Tests/Serializer/JsonNetSerializerTests.cs +++ b/tests/Foundatio.Tests/Serializer/JsonNetSerializerTests.cs @@ -4,48 +4,47 @@ using Xunit; using Xunit.Abstractions; -namespace Foundatio.Tests.Serializer +namespace Foundatio.Tests.Serializer; + +public class JsonNetSerializerTests : SerializerTestsBase { - public class JsonNetSerializerTests : SerializerTestsBase + public JsonNetSerializerTests(ITestOutputHelper output) : base(output) { } + + protected override ISerializer GetSerializer() + { + return new JsonNetSerializer(); + } + + [Fact] + public override void CanRoundTripBytes() + { + base.CanRoundTripBytes(); + } + + [Fact] + public override void CanRoundTripString() { - public JsonNetSerializerTests(ITestOutputHelper output) : base(output) { } - - protected override ISerializer GetSerializer() - { - return new JsonNetSerializer(); - } - - [Fact] - public override void CanRoundTripBytes() - { - base.CanRoundTripBytes(); - } - - [Fact] - public override void CanRoundTripString() - { - base.CanRoundTripString(); - } - - [Fact] - public override void CanHandlePrimitiveTypes() - { - base.CanHandlePrimitiveTypes(); - } - - [Fact(Skip = "Skip benchmarks for now")] - public virtual void Benchmark() - { - var summary = BenchmarkDotNet.Running.BenchmarkRunner.Run(); - _logger.LogInformation(summary.ToJson()); - } + base.CanRoundTripString(); } - public class JsonNetSerializerBenchmark : SerializerBenchmarkBase + [Fact] + public override void CanHandlePrimitiveTypes() + { + base.CanHandlePrimitiveTypes(); + } + + [Fact(Skip = "Skip benchmarks for now")] + public virtual void Benchmark() + { + var summary = BenchmarkDotNet.Running.BenchmarkRunner.Run(); + _logger.LogInformation(summary.ToJson()); + } +} + +public class JsonNetSerializerBenchmark : SerializerBenchmarkBase +{ + protected override ISerializer GetSerializer() { - protected override ISerializer GetSerializer() - { - return new JsonNetSerializer(); - } + return new JsonNetSerializer(); } } diff --git a/tests/Foundatio.Tests/Serializer/MessagePackSerializerTests.cs b/tests/Foundatio.Tests/Serializer/MessagePackSerializerTests.cs index bfd5b810c..aa4ab44af 100644 --- a/tests/Foundatio.Tests/Serializer/MessagePackSerializerTests.cs +++ b/tests/Foundatio.Tests/Serializer/MessagePackSerializerTests.cs @@ -4,48 +4,47 @@ using Xunit; using Xunit.Abstractions; -namespace Foundatio.Tests.Serializer +namespace Foundatio.Tests.Serializer; + +public class MessagePackSerializerTests : SerializerTestsBase { - public class MessagePackSerializerTests : SerializerTestsBase + public MessagePackSerializerTests(ITestOutputHelper output) : base(output) { } + + protected override ISerializer GetSerializer() + { + return new MessagePackSerializer(); + } + + [Fact] + public override void CanRoundTripBytes() + { + base.CanRoundTripBytes(); + } + + [Fact] + public override void CanRoundTripString() { - public MessagePackSerializerTests(ITestOutputHelper output) : base(output) { } - - protected override ISerializer GetSerializer() - { - return new MessagePackSerializer(); - } - - [Fact] - public override void CanRoundTripBytes() - { - base.CanRoundTripBytes(); - } - - [Fact] - public override void CanRoundTripString() - { - base.CanRoundTripString(); - } - - [Fact] - public override void CanHandlePrimitiveTypes() - { - base.CanHandlePrimitiveTypes(); - } - - [Fact(Skip = "Skip benchmarks for now")] - public virtual void Benchmark() - { - var summary = BenchmarkDotNet.Running.BenchmarkRunner.Run(); - _logger.LogInformation(summary.ToJson()); - } + base.CanRoundTripString(); } - public class MessagePackSerializerBenchmark : SerializerBenchmarkBase + [Fact] + public override void CanHandlePrimitiveTypes() + { + base.CanHandlePrimitiveTypes(); + } + + [Fact(Skip = "Skip benchmarks for now")] + public virtual void Benchmark() + { + var summary = BenchmarkDotNet.Running.BenchmarkRunner.Run(); + _logger.LogInformation(summary.ToJson()); + } +} + +public class MessagePackSerializerBenchmark : SerializerBenchmarkBase +{ + protected override ISerializer GetSerializer() { - protected override ISerializer GetSerializer() - { - return new MessagePackSerializer(); - } + return new MessagePackSerializer(); } } diff --git a/tests/Foundatio.Tests/Serializer/SystemTextJsonSerializerTests.cs b/tests/Foundatio.Tests/Serializer/SystemTextJsonSerializerTests.cs index 91d1da200..55ec077ca 100644 --- a/tests/Foundatio.Tests/Serializer/SystemTextJsonSerializerTests.cs +++ b/tests/Foundatio.Tests/Serializer/SystemTextJsonSerializerTests.cs @@ -4,48 +4,47 @@ using Xunit; using Xunit.Abstractions; -namespace Foundatio.Tests.Serializer +namespace Foundatio.Tests.Serializer; + +public class SystemTextJsonSerializerTests : SerializerTestsBase { - public class SystemTextJsonSerializerTests : SerializerTestsBase + public SystemTextJsonSerializerTests(ITestOutputHelper output) : base(output) { } + + protected override ISerializer GetSerializer() + { + return new SystemTextJsonSerializer(); + } + + [Fact] + public override void CanRoundTripBytes() + { + base.CanRoundTripBytes(); + } + + [Fact] + public override void CanRoundTripString() { - public SystemTextJsonSerializerTests(ITestOutputHelper output) : base(output) { } - - protected override ISerializer GetSerializer() - { - return new SystemTextJsonSerializer(); - } - - [Fact] - public override void CanRoundTripBytes() - { - base.CanRoundTripBytes(); - } - - [Fact] - public override void CanRoundTripString() - { - base.CanRoundTripString(); - } - - [Fact] - public override void CanHandlePrimitiveTypes() - { - base.CanHandlePrimitiveTypes(); - } - - [Fact(Skip = "Skip benchmarks for now")] - public virtual void Benchmark() - { - var summary = BenchmarkDotNet.Running.BenchmarkRunner.Run(); - _logger.LogInformation(summary.ToJson()); - } + base.CanRoundTripString(); } - public class SystemTextJsonSerializerBenchmark : SerializerBenchmarkBase + [Fact] + public override void CanHandlePrimitiveTypes() + { + base.CanHandlePrimitiveTypes(); + } + + [Fact(Skip = "Skip benchmarks for now")] + public virtual void Benchmark() + { + var summary = BenchmarkDotNet.Running.BenchmarkRunner.Run(); + _logger.LogInformation(summary.ToJson()); + } +} + +public class SystemTextJsonSerializerBenchmark : SerializerBenchmarkBase +{ + protected override ISerializer GetSerializer() { - protected override ISerializer GetSerializer() - { - return new SystemTextJsonSerializer(); - } + return new SystemTextJsonSerializer(); } } diff --git a/tests/Foundatio.Tests/Serializer/Utf8JsonSerializerTests.cs b/tests/Foundatio.Tests/Serializer/Utf8JsonSerializerTests.cs index b4c3793ec..b50c6ca03 100644 --- a/tests/Foundatio.Tests/Serializer/Utf8JsonSerializerTests.cs +++ b/tests/Foundatio.Tests/Serializer/Utf8JsonSerializerTests.cs @@ -4,42 +4,41 @@ using Xunit; using Xunit.Abstractions; -namespace Foundatio.Tests.Serializer +namespace Foundatio.Tests.Serializer; + +public class Utf8JsonSerializerTests : SerializerTestsBase { - public class Utf8JsonSerializerTests : SerializerTestsBase - { - public Utf8JsonSerializerTests(ITestOutputHelper output) : base(output) { } + public Utf8JsonSerializerTests(ITestOutputHelper output) : base(output) { } - protected override ISerializer GetSerializer() - { - return new Utf8JsonSerializer(); - } + protected override ISerializer GetSerializer() + { + return new Utf8JsonSerializer(); + } - [Fact] - public override void CanRoundTripBytes() - { - base.CanRoundTripBytes(); - } + [Fact] + public override void CanRoundTripBytes() + { + base.CanRoundTripBytes(); + } - [Fact] - public override void CanRoundTripString() - { - base.CanRoundTripString(); - } + [Fact] + public override void CanRoundTripString() + { + base.CanRoundTripString(); + } - [Fact(Skip = "Skip benchmarks for now")] - public virtual void Benchmark() - { - var summary = BenchmarkDotNet.Running.BenchmarkRunner.Run(); - _logger.LogInformation(summary.ToJson()); - } + [Fact(Skip = "Skip benchmarks for now")] + public virtual void Benchmark() + { + var summary = BenchmarkDotNet.Running.BenchmarkRunner.Run(); + _logger.LogInformation(summary.ToJson()); } +} - public class Utf8JsonSerializerBenchmark : SerializerBenchmarkBase +public class Utf8JsonSerializerBenchmark : SerializerBenchmarkBase +{ + protected override ISerializer GetSerializer() { - protected override ISerializer GetSerializer() - { - return new Utf8JsonSerializer(); - } + return new Utf8JsonSerializer(); } } diff --git a/tests/Foundatio.Tests/Storage/FolderFileStorageTests.cs b/tests/Foundatio.Tests/Storage/FolderFileStorageTests.cs index 4d40a6d08..b2f4fbc5e 100644 --- a/tests/Foundatio.Tests/Storage/FolderFileStorageTests.cs +++ b/tests/Foundatio.Tests/Storage/FolderFileStorageTests.cs @@ -3,135 +3,134 @@ using Xunit; using Xunit.Abstractions; -namespace Foundatio.Tests.Storage +namespace Foundatio.Tests.Storage; + +public class FolderFileStorageTests : FileStorageTestsBase { - public class FolderFileStorageTests : FileStorageTestsBase - { - public FolderFileStorageTests(ITestOutputHelper output) : base(output) { } - - protected override IFileStorage GetStorage() - { - return new FolderFileStorage(o => o.Folder("|DataDirectory|\\temp")); - } - - [Fact] - public override Task CanGetEmptyFileListOnMissingDirectoryAsync() - { - return base.CanGetEmptyFileListOnMissingDirectoryAsync(); - } - - [Fact] - public override Task CanGetFileListForSingleFolderAsync() - { - return base.CanGetFileListForSingleFolderAsync(); - } - - [Fact] - public override Task CanGetPagedFileListForSingleFolderAsync() - { - return base.CanGetPagedFileListForSingleFolderAsync(); - } - - [Fact] - public override Task CanGetFileListForSingleFileAsync() - { - return base.CanGetFileListForSingleFileAsync(); - } - - [Fact] - public override Task CanGetFileInfoAsync() - { - return base.CanGetFileInfoAsync(); - } - - [Fact] - public override Task CanGetNonExistentFileInfoAsync() - { - return base.CanGetNonExistentFileInfoAsync(); - } - - [Fact] - public override Task CanSaveFilesAsync() - { - return base.CanSaveFilesAsync(); - } - - [Fact] - public override Task CanManageFilesAsync() - { - return base.CanManageFilesAsync(); - } - - [Fact] - public override Task CanRenameFilesAsync() - { - return base.CanRenameFilesAsync(); - } - - [Fact] - public override Task CanConcurrentlyManageFilesAsync() - { - return base.CanConcurrentlyManageFilesAsync(); - } - - [Fact] - public override void CanUseDataDirectory() - { - base.CanUseDataDirectory(); - } - - [Fact] - public override Task CanDeleteEntireFolderAsync() - { - return base.CanDeleteEntireFolderAsync(); - } - - [Fact] - public override Task CanDeleteEntireFolderWithWildcardAsync() - { - return base.CanDeleteEntireFolderWithWildcardAsync(); - } - - [Fact(Skip = "Directory.EnumerateFiles does not support nested folder wildcards")] - public override Task CanDeleteFolderWithMultiFolderWildcardsAsync() - { - return base.CanDeleteFolderWithMultiFolderWildcardsAsync(); - } - - [Fact] - public override Task CanDeleteSpecificFilesAsync() - { - return base.CanDeleteSpecificFilesAsync(); - } - - [Fact] - public override Task CanDeleteNestedFolderAsync() - { - return base.CanDeleteNestedFolderAsync(); - } - - [Fact] - public override Task CanDeleteSpecificFilesInNestedFolderAsync() - { - return base.CanDeleteSpecificFilesInNestedFolderAsync(); - } - - [Fact] - public override Task CanRoundTripSeekableStreamAsync() - { - return base.CanRoundTripSeekableStreamAsync(); - } - - [Fact] - public override Task WillRespectStreamOffsetAsync() - { - return base.WillRespectStreamOffsetAsync(); - } - - [Fact] - public override Task WillWriteStreamContentAsync() - { - return base.WillWriteStreamContentAsync(); - } + public FolderFileStorageTests(ITestOutputHelper output) : base(output) { } + + protected override IFileStorage GetStorage() + { + return new FolderFileStorage(o => o.Folder("|DataDirectory|\\temp")); + } + + [Fact] + public override Task CanGetEmptyFileListOnMissingDirectoryAsync() + { + return base.CanGetEmptyFileListOnMissingDirectoryAsync(); + } + + [Fact] + public override Task CanGetFileListForSingleFolderAsync() + { + return base.CanGetFileListForSingleFolderAsync(); + } + + [Fact] + public override Task CanGetPagedFileListForSingleFolderAsync() + { + return base.CanGetPagedFileListForSingleFolderAsync(); + } + + [Fact] + public override Task CanGetFileListForSingleFileAsync() + { + return base.CanGetFileListForSingleFileAsync(); + } + + [Fact] + public override Task CanGetFileInfoAsync() + { + return base.CanGetFileInfoAsync(); + } + + [Fact] + public override Task CanGetNonExistentFileInfoAsync() + { + return base.CanGetNonExistentFileInfoAsync(); + } + + [Fact] + public override Task CanSaveFilesAsync() + { + return base.CanSaveFilesAsync(); + } + + [Fact] + public override Task CanManageFilesAsync() + { + return base.CanManageFilesAsync(); + } + + [Fact] + public override Task CanRenameFilesAsync() + { + return base.CanRenameFilesAsync(); + } + + [Fact] + public override Task CanConcurrentlyManageFilesAsync() + { + return base.CanConcurrentlyManageFilesAsync(); + } + + [Fact] + public override void CanUseDataDirectory() + { + base.CanUseDataDirectory(); + } + + [Fact] + public override Task CanDeleteEntireFolderAsync() + { + return base.CanDeleteEntireFolderAsync(); + } + + [Fact] + public override Task CanDeleteEntireFolderWithWildcardAsync() + { + return base.CanDeleteEntireFolderWithWildcardAsync(); + } + + [Fact(Skip = "Directory.EnumerateFiles does not support nested folder wildcards")] + public override Task CanDeleteFolderWithMultiFolderWildcardsAsync() + { + return base.CanDeleteFolderWithMultiFolderWildcardsAsync(); + } + + [Fact] + public override Task CanDeleteSpecificFilesAsync() + { + return base.CanDeleteSpecificFilesAsync(); + } + + [Fact] + public override Task CanDeleteNestedFolderAsync() + { + return base.CanDeleteNestedFolderAsync(); + } + + [Fact] + public override Task CanDeleteSpecificFilesInNestedFolderAsync() + { + return base.CanDeleteSpecificFilesInNestedFolderAsync(); + } + + [Fact] + public override Task CanRoundTripSeekableStreamAsync() + { + return base.CanRoundTripSeekableStreamAsync(); + } + + [Fact] + public override Task WillRespectStreamOffsetAsync() + { + return base.WillRespectStreamOffsetAsync(); + } + + [Fact] + public override Task WillWriteStreamContentAsync() + { + return base.WillWriteStreamContentAsync(); } } diff --git a/tests/Foundatio.Tests/Storage/InMemoryFileStorageTests.cs b/tests/Foundatio.Tests/Storage/InMemoryFileStorageTests.cs index 0a65f2786..d25c45ef0 100644 --- a/tests/Foundatio.Tests/Storage/InMemoryFileStorageTests.cs +++ b/tests/Foundatio.Tests/Storage/InMemoryFileStorageTests.cs @@ -3,135 +3,134 @@ using Xunit; using Xunit.Abstractions; -namespace Foundatio.Tests.Storage +namespace Foundatio.Tests.Storage; + +public class InMemoryFileStorageTests : FileStorageTestsBase { - public class InMemoryFileStorageTests : FileStorageTestsBase - { - public InMemoryFileStorageTests(ITestOutputHelper output) : base(output) { } - - protected override IFileStorage GetStorage() - { - return new InMemoryFileStorage { MaxFiles = 2000 }; - } - - [Fact] - public override Task CanGetEmptyFileListOnMissingDirectoryAsync() - { - return base.CanGetEmptyFileListOnMissingDirectoryAsync(); - } - - [Fact] - public override Task CanGetFileListForSingleFolderAsync() - { - return base.CanGetFileListForSingleFolderAsync(); - } - - [Fact] - public override Task CanGetFileListForSingleFileAsync() - { - return base.CanGetFileListForSingleFileAsync(); - } - - [Fact] - public override Task CanGetPagedFileListForSingleFolderAsync() - { - return base.CanGetPagedFileListForSingleFolderAsync(); - } - - [Fact] - public override Task CanGetFileInfoAsync() - { - return base.CanGetFileInfoAsync(); - } - - [Fact] - public override Task CanGetNonExistentFileInfoAsync() - { - return base.CanGetNonExistentFileInfoAsync(); - } - - [Fact] - public override Task CanSaveFilesAsync() - { - return base.CanSaveFilesAsync(); - } - - [Fact] - public override Task CanManageFilesAsync() - { - return base.CanManageFilesAsync(); - } - - [Fact] - public override Task CanRenameFilesAsync() - { - return base.CanRenameFilesAsync(); - } - - [Fact] - public override Task CanConcurrentlyManageFilesAsync() - { - return base.CanConcurrentlyManageFilesAsync(); - } - - [Fact] - public override void CanUseDataDirectory() - { - base.CanUseDataDirectory(); - } - - [Fact] - public override Task CanDeleteEntireFolderAsync() - { - return base.CanDeleteEntireFolderAsync(); - } - - [Fact] - public override Task CanDeleteEntireFolderWithWildcardAsync() - { - return base.CanDeleteEntireFolderWithWildcardAsync(); - } - - [Fact] - public override Task CanDeleteFolderWithMultiFolderWildcardsAsync() - { - return base.CanDeleteFolderWithMultiFolderWildcardsAsync(); - } - - [Fact] - public override Task CanDeleteSpecificFilesAsync() - { - return base.CanDeleteSpecificFilesAsync(); - } - - [Fact] - public override Task CanDeleteNestedFolderAsync() - { - return base.CanDeleteNestedFolderAsync(); - } - - [Fact] - public override Task CanDeleteSpecificFilesInNestedFolderAsync() - { - return base.CanDeleteSpecificFilesInNestedFolderAsync(); - } - - [Fact] - public override Task CanRoundTripSeekableStreamAsync() - { - return base.CanRoundTripSeekableStreamAsync(); - } - - [Fact] - public override Task WillRespectStreamOffsetAsync() - { - return base.WillRespectStreamOffsetAsync(); - } - - [Fact] - public override Task WillWriteStreamContentAsync() - { - return base.WillWriteStreamContentAsync(); - } + public InMemoryFileStorageTests(ITestOutputHelper output) : base(output) { } + + protected override IFileStorage GetStorage() + { + return new InMemoryFileStorage { MaxFiles = 2000 }; + } + + [Fact] + public override Task CanGetEmptyFileListOnMissingDirectoryAsync() + { + return base.CanGetEmptyFileListOnMissingDirectoryAsync(); + } + + [Fact] + public override Task CanGetFileListForSingleFolderAsync() + { + return base.CanGetFileListForSingleFolderAsync(); + } + + [Fact] + public override Task CanGetFileListForSingleFileAsync() + { + return base.CanGetFileListForSingleFileAsync(); + } + + [Fact] + public override Task CanGetPagedFileListForSingleFolderAsync() + { + return base.CanGetPagedFileListForSingleFolderAsync(); + } + + [Fact] + public override Task CanGetFileInfoAsync() + { + return base.CanGetFileInfoAsync(); + } + + [Fact] + public override Task CanGetNonExistentFileInfoAsync() + { + return base.CanGetNonExistentFileInfoAsync(); + } + + [Fact] + public override Task CanSaveFilesAsync() + { + return base.CanSaveFilesAsync(); + } + + [Fact] + public override Task CanManageFilesAsync() + { + return base.CanManageFilesAsync(); + } + + [Fact] + public override Task CanRenameFilesAsync() + { + return base.CanRenameFilesAsync(); + } + + [Fact] + public override Task CanConcurrentlyManageFilesAsync() + { + return base.CanConcurrentlyManageFilesAsync(); + } + + [Fact] + public override void CanUseDataDirectory() + { + base.CanUseDataDirectory(); + } + + [Fact] + public override Task CanDeleteEntireFolderAsync() + { + return base.CanDeleteEntireFolderAsync(); + } + + [Fact] + public override Task CanDeleteEntireFolderWithWildcardAsync() + { + return base.CanDeleteEntireFolderWithWildcardAsync(); + } + + [Fact] + public override Task CanDeleteFolderWithMultiFolderWildcardsAsync() + { + return base.CanDeleteFolderWithMultiFolderWildcardsAsync(); + } + + [Fact] + public override Task CanDeleteSpecificFilesAsync() + { + return base.CanDeleteSpecificFilesAsync(); + } + + [Fact] + public override Task CanDeleteNestedFolderAsync() + { + return base.CanDeleteNestedFolderAsync(); + } + + [Fact] + public override Task CanDeleteSpecificFilesInNestedFolderAsync() + { + return base.CanDeleteSpecificFilesInNestedFolderAsync(); + } + + [Fact] + public override Task CanRoundTripSeekableStreamAsync() + { + return base.CanRoundTripSeekableStreamAsync(); + } + + [Fact] + public override Task WillRespectStreamOffsetAsync() + { + return base.WillRespectStreamOffsetAsync(); + } + + [Fact] + public override Task WillWriteStreamContentAsync() + { + return base.WillWriteStreamContentAsync(); } } diff --git a/tests/Foundatio.Tests/Storage/ScopedFolderFileStorageTests.cs b/tests/Foundatio.Tests/Storage/ScopedFolderFileStorageTests.cs index c198a9500..a2e324a3b 100644 --- a/tests/Foundatio.Tests/Storage/ScopedFolderFileStorageTests.cs +++ b/tests/Foundatio.Tests/Storage/ScopedFolderFileStorageTests.cs @@ -3,135 +3,134 @@ using Xunit; using Xunit.Abstractions; -namespace Foundatio.Tests.Storage +namespace Foundatio.Tests.Storage; + +public class ScopedFolderFileStorageTests : FileStorageTestsBase { - public class ScopedFolderFileStorageTests : FileStorageTestsBase - { - public ScopedFolderFileStorageTests(ITestOutputHelper output) : base(output) { } - - protected override IFileStorage GetStorage() - { - return new ScopedFileStorage(new FolderFileStorage(o => o.Folder("|DataDirectory|\\temp")), "scoped"); - } - - [Fact] - public override Task CanGetEmptyFileListOnMissingDirectoryAsync() - { - return base.CanGetEmptyFileListOnMissingDirectoryAsync(); - } - - [Fact] - public override Task CanGetFileListForSingleFolderAsync() - { - return base.CanGetFileListForSingleFolderAsync(); - } - - [Fact] - public override Task CanGetFileListForSingleFileAsync() - { - return base.CanGetFileListForSingleFileAsync(); - } - - [Fact] - public override Task CanGetPagedFileListForSingleFolderAsync() - { - return base.CanGetPagedFileListForSingleFolderAsync(); - } - - [Fact] - public override Task CanGetFileInfoAsync() - { - return base.CanGetFileInfoAsync(); - } - - [Fact] - public override Task CanGetNonExistentFileInfoAsync() - { - return base.CanGetNonExistentFileInfoAsync(); - } - - [Fact] - public override Task CanSaveFilesAsync() - { - return base.CanSaveFilesAsync(); - } - - [Fact] - public override Task CanManageFilesAsync() - { - return base.CanManageFilesAsync(); - } - - [Fact] - public override Task CanRenameFilesAsync() - { - return base.CanRenameFilesAsync(); - } - - [Fact] - public override Task CanConcurrentlyManageFilesAsync() - { - return base.CanConcurrentlyManageFilesAsync(); - } - - [Fact] - public override void CanUseDataDirectory() - { - base.CanUseDataDirectory(); - } - - [Fact] - public override Task CanDeleteEntireFolderAsync() - { - return base.CanDeleteEntireFolderAsync(); - } - - [Fact] - public override Task CanDeleteEntireFolderWithWildcardAsync() - { - return base.CanDeleteEntireFolderWithWildcardAsync(); - } - - [Fact(Skip = "Directory.EnumerateFiles does not support nested folder wildcards")] - public override Task CanDeleteFolderWithMultiFolderWildcardsAsync() - { - return base.CanDeleteFolderWithMultiFolderWildcardsAsync(); - } - - [Fact] - public override Task CanDeleteSpecificFilesAsync() - { - return base.CanDeleteSpecificFilesAsync(); - } - - [Fact] - public override Task CanDeleteNestedFolderAsync() - { - return base.CanDeleteNestedFolderAsync(); - } - - [Fact] - public override Task CanDeleteSpecificFilesInNestedFolderAsync() - { - return base.CanDeleteSpecificFilesInNestedFolderAsync(); - } - - [Fact] - public override Task CanRoundTripSeekableStreamAsync() - { - return base.CanRoundTripSeekableStreamAsync(); - } - - [Fact] - public override Task WillRespectStreamOffsetAsync() - { - return base.WillRespectStreamOffsetAsync(); - } - - [Fact] - public override Task WillWriteStreamContentAsync() - { - return base.WillWriteStreamContentAsync(); - } + public ScopedFolderFileStorageTests(ITestOutputHelper output) : base(output) { } + + protected override IFileStorage GetStorage() + { + return new ScopedFileStorage(new FolderFileStorage(o => o.Folder("|DataDirectory|\\temp")), "scoped"); + } + + [Fact] + public override Task CanGetEmptyFileListOnMissingDirectoryAsync() + { + return base.CanGetEmptyFileListOnMissingDirectoryAsync(); + } + + [Fact] + public override Task CanGetFileListForSingleFolderAsync() + { + return base.CanGetFileListForSingleFolderAsync(); + } + + [Fact] + public override Task CanGetFileListForSingleFileAsync() + { + return base.CanGetFileListForSingleFileAsync(); + } + + [Fact] + public override Task CanGetPagedFileListForSingleFolderAsync() + { + return base.CanGetPagedFileListForSingleFolderAsync(); + } + + [Fact] + public override Task CanGetFileInfoAsync() + { + return base.CanGetFileInfoAsync(); + } + + [Fact] + public override Task CanGetNonExistentFileInfoAsync() + { + return base.CanGetNonExistentFileInfoAsync(); + } + + [Fact] + public override Task CanSaveFilesAsync() + { + return base.CanSaveFilesAsync(); + } + + [Fact] + public override Task CanManageFilesAsync() + { + return base.CanManageFilesAsync(); + } + + [Fact] + public override Task CanRenameFilesAsync() + { + return base.CanRenameFilesAsync(); + } + + [Fact] + public override Task CanConcurrentlyManageFilesAsync() + { + return base.CanConcurrentlyManageFilesAsync(); + } + + [Fact] + public override void CanUseDataDirectory() + { + base.CanUseDataDirectory(); + } + + [Fact] + public override Task CanDeleteEntireFolderAsync() + { + return base.CanDeleteEntireFolderAsync(); + } + + [Fact] + public override Task CanDeleteEntireFolderWithWildcardAsync() + { + return base.CanDeleteEntireFolderWithWildcardAsync(); + } + + [Fact(Skip = "Directory.EnumerateFiles does not support nested folder wildcards")] + public override Task CanDeleteFolderWithMultiFolderWildcardsAsync() + { + return base.CanDeleteFolderWithMultiFolderWildcardsAsync(); + } + + [Fact] + public override Task CanDeleteSpecificFilesAsync() + { + return base.CanDeleteSpecificFilesAsync(); + } + + [Fact] + public override Task CanDeleteNestedFolderAsync() + { + return base.CanDeleteNestedFolderAsync(); + } + + [Fact] + public override Task CanDeleteSpecificFilesInNestedFolderAsync() + { + return base.CanDeleteSpecificFilesInNestedFolderAsync(); + } + + [Fact] + public override Task CanRoundTripSeekableStreamAsync() + { + return base.CanRoundTripSeekableStreamAsync(); + } + + [Fact] + public override Task WillRespectStreamOffsetAsync() + { + return base.WillRespectStreamOffsetAsync(); + } + + [Fact] + public override Task WillWriteStreamContentAsync() + { + return base.WillWriteStreamContentAsync(); } } diff --git a/tests/Foundatio.Tests/Storage/ScopedInMemoryFileStorageTests.cs b/tests/Foundatio.Tests/Storage/ScopedInMemoryFileStorageTests.cs index 1da6c9eff..f22449fdd 100644 --- a/tests/Foundatio.Tests/Storage/ScopedInMemoryFileStorageTests.cs +++ b/tests/Foundatio.Tests/Storage/ScopedInMemoryFileStorageTests.cs @@ -3,135 +3,134 @@ using Xunit; using Xunit.Abstractions; -namespace Foundatio.Tests.Storage +namespace Foundatio.Tests.Storage; + +public class ScopedInMemoryFileStorageTests : FileStorageTestsBase { - public class ScopedInMemoryFileStorageTests : FileStorageTestsBase - { - public ScopedInMemoryFileStorageTests(ITestOutputHelper output) : base(output) { } - - protected override IFileStorage GetStorage() - { - return new ScopedFileStorage(new InMemoryFileStorage { MaxFiles = 2000 }, "scoped"); - } - - [Fact] - public override Task CanGetEmptyFileListOnMissingDirectoryAsync() - { - return base.CanGetEmptyFileListOnMissingDirectoryAsync(); - } - - [Fact] - public override Task CanGetFileListForSingleFolderAsync() - { - return base.CanGetFileListForSingleFolderAsync(); - } - - [Fact] - public override Task CanGetFileListForSingleFileAsync() - { - return base.CanGetFileListForSingleFileAsync(); - } - - [Fact] - public override Task CanGetPagedFileListForSingleFolderAsync() - { - return base.CanGetPagedFileListForSingleFolderAsync(); - } - - [Fact] - public override Task CanGetFileInfoAsync() - { - return base.CanGetFileInfoAsync(); - } - - [Fact] - public override Task CanGetNonExistentFileInfoAsync() - { - return base.CanGetNonExistentFileInfoAsync(); - } - - [Fact] - public override Task CanSaveFilesAsync() - { - return base.CanSaveFilesAsync(); - } - - [Fact] - public override Task CanManageFilesAsync() - { - return base.CanManageFilesAsync(); - } - - [Fact] - public override Task CanRenameFilesAsync() - { - return base.CanRenameFilesAsync(); - } - - [Fact] - public override Task CanConcurrentlyManageFilesAsync() - { - return base.CanConcurrentlyManageFilesAsync(); - } - - [Fact] - public override void CanUseDataDirectory() - { - base.CanUseDataDirectory(); - } - - [Fact] - public override Task CanDeleteEntireFolderAsync() - { - return base.CanDeleteEntireFolderAsync(); - } - - [Fact] - public override Task CanDeleteEntireFolderWithWildcardAsync() - { - return base.CanDeleteEntireFolderWithWildcardAsync(); - } - - [Fact] - public override Task CanDeleteFolderWithMultiFolderWildcardsAsync() - { - return base.CanDeleteFolderWithMultiFolderWildcardsAsync(); - } - - [Fact] - public override Task CanDeleteSpecificFilesAsync() - { - return base.CanDeleteSpecificFilesAsync(); - } - - [Fact] - public override Task CanDeleteNestedFolderAsync() - { - return base.CanDeleteNestedFolderAsync(); - } - - [Fact] - public override Task CanDeleteSpecificFilesInNestedFolderAsync() - { - return base.CanDeleteSpecificFilesInNestedFolderAsync(); - } - - [Fact] - public override Task CanRoundTripSeekableStreamAsync() - { - return base.CanRoundTripSeekableStreamAsync(); - } - - [Fact] - public override Task WillRespectStreamOffsetAsync() - { - return base.WillRespectStreamOffsetAsync(); - } - - [Fact] - public override Task WillWriteStreamContentAsync() - { - return base.WillWriteStreamContentAsync(); - } + public ScopedInMemoryFileStorageTests(ITestOutputHelper output) : base(output) { } + + protected override IFileStorage GetStorage() + { + return new ScopedFileStorage(new InMemoryFileStorage { MaxFiles = 2000 }, "scoped"); + } + + [Fact] + public override Task CanGetEmptyFileListOnMissingDirectoryAsync() + { + return base.CanGetEmptyFileListOnMissingDirectoryAsync(); + } + + [Fact] + public override Task CanGetFileListForSingleFolderAsync() + { + return base.CanGetFileListForSingleFolderAsync(); + } + + [Fact] + public override Task CanGetFileListForSingleFileAsync() + { + return base.CanGetFileListForSingleFileAsync(); + } + + [Fact] + public override Task CanGetPagedFileListForSingleFolderAsync() + { + return base.CanGetPagedFileListForSingleFolderAsync(); + } + + [Fact] + public override Task CanGetFileInfoAsync() + { + return base.CanGetFileInfoAsync(); + } + + [Fact] + public override Task CanGetNonExistentFileInfoAsync() + { + return base.CanGetNonExistentFileInfoAsync(); + } + + [Fact] + public override Task CanSaveFilesAsync() + { + return base.CanSaveFilesAsync(); + } + + [Fact] + public override Task CanManageFilesAsync() + { + return base.CanManageFilesAsync(); + } + + [Fact] + public override Task CanRenameFilesAsync() + { + return base.CanRenameFilesAsync(); + } + + [Fact] + public override Task CanConcurrentlyManageFilesAsync() + { + return base.CanConcurrentlyManageFilesAsync(); + } + + [Fact] + public override void CanUseDataDirectory() + { + base.CanUseDataDirectory(); + } + + [Fact] + public override Task CanDeleteEntireFolderAsync() + { + return base.CanDeleteEntireFolderAsync(); + } + + [Fact] + public override Task CanDeleteEntireFolderWithWildcardAsync() + { + return base.CanDeleteEntireFolderWithWildcardAsync(); + } + + [Fact] + public override Task CanDeleteFolderWithMultiFolderWildcardsAsync() + { + return base.CanDeleteFolderWithMultiFolderWildcardsAsync(); + } + + [Fact] + public override Task CanDeleteSpecificFilesAsync() + { + return base.CanDeleteSpecificFilesAsync(); + } + + [Fact] + public override Task CanDeleteNestedFolderAsync() + { + return base.CanDeleteNestedFolderAsync(); + } + + [Fact] + public override Task CanDeleteSpecificFilesInNestedFolderAsync() + { + return base.CanDeleteSpecificFilesInNestedFolderAsync(); + } + + [Fact] + public override Task CanRoundTripSeekableStreamAsync() + { + return base.CanRoundTripSeekableStreamAsync(); + } + + [Fact] + public override Task WillRespectStreamOffsetAsync() + { + return base.WillRespectStreamOffsetAsync(); + } + + [Fact] + public override Task WillWriteStreamContentAsync() + { + return base.WillWriteStreamContentAsync(); } } diff --git a/tests/Foundatio.Tests/Utility/CloneTests.cs b/tests/Foundatio.Tests/Utility/CloneTests.cs index 50bede077..628da8ee9 100644 --- a/tests/Foundatio.Tests/Utility/CloneTests.cs +++ b/tests/Foundatio.Tests/Utility/CloneTests.cs @@ -7,99 +7,98 @@ using Xunit; using Xunit.Abstractions; -namespace Foundatio.Tests.Utility +namespace Foundatio.Tests.Utility; + +public class CloneTests : TestWithLoggingBase { - public class CloneTests : TestWithLoggingBase - { - public CloneTests(ITestOutputHelper output) : base(output) { } + public CloneTests(ITestOutputHelper output) : base(output) { } - [Fact] - public void CanCloneModel() + [Fact] + public void CanCloneModel() + { + var model = new CloneModel { - var model = new CloneModel - { - IntProperty = 1, - StringProperty = "test", - ListProperty = new List { 1 }, - HashSet = new HashSet(), - ObjectProperty = new CloneModel { IntProperty = 1 } - }; + IntProperty = 1, + StringProperty = "test", + ListProperty = new List { 1 }, + HashSet = new HashSet(), + ObjectProperty = new CloneModel { IntProperty = 1 } + }; - var cloned = model.DeepClone(); - Assert.Equal(model.IntProperty, cloned.IntProperty); - Assert.Equal(model.StringProperty, cloned.StringProperty); - Assert.Equal(model.ListProperty, cloned.ListProperty); - Assert.Equal(model.EmptyStringList, cloned.EmptyStringList); - Assert.Equal(model.EmptyHashSet, cloned.EmptyHashSet); - Assert.Equal(model.HashSet, cloned.HashSet); - Assert.Equal(((CloneModel)model.ObjectProperty).IntProperty, ((CloneModel)model.ObjectProperty).IntProperty); - } + var cloned = model.DeepClone(); + Assert.Equal(model.IntProperty, cloned.IntProperty); + Assert.Equal(model.StringProperty, cloned.StringProperty); + Assert.Equal(model.ListProperty, cloned.ListProperty); + Assert.Equal(model.EmptyStringList, cloned.EmptyStringList); + Assert.Equal(model.EmptyHashSet, cloned.EmptyHashSet); + Assert.Equal(model.HashSet, cloned.HashSet); + Assert.Equal(((CloneModel)model.ObjectProperty).IntProperty, ((CloneModel)model.ObjectProperty).IntProperty); + } - [Fact] - public void CanCloneJsonSerializedModel() + [Fact] + public void CanCloneJsonSerializedModel() + { + var model = new CloneModel { - var model = new CloneModel - { - IntProperty = 1, - StringProperty = "test", - ListProperty = new List { 1 }, - ObjectProperty = new CloneModel { IntProperty = 1 } - }; + IntProperty = 1, + StringProperty = "test", + ListProperty = new List { 1 }, + ObjectProperty = new CloneModel { IntProperty = 1 } + }; - var serializer = new JsonNetSerializer(); - var result = serializer.SerializeToBytes(model); - var deserialized = serializer.Deserialize(result); - Assert.Equal(model.IntProperty, deserialized.IntProperty); - Assert.Equal(model.StringProperty, deserialized.StringProperty); - Assert.Equal(model.ListProperty, deserialized.ListProperty); - var dm = ((JToken)deserialized.ObjectProperty).ToObject(); - Assert.Equal(((CloneModel)model.ObjectProperty).IntProperty, dm.IntProperty); + var serializer = new JsonNetSerializer(); + var result = serializer.SerializeToBytes(model); + var deserialized = serializer.Deserialize(result); + Assert.Equal(model.IntProperty, deserialized.IntProperty); + Assert.Equal(model.StringProperty, deserialized.StringProperty); + Assert.Equal(model.ListProperty, deserialized.ListProperty); + var dm = ((JToken)deserialized.ObjectProperty).ToObject(); + Assert.Equal(((CloneModel)model.ObjectProperty).IntProperty, dm.IntProperty); - var cloned = deserialized.DeepClone(); - Assert.Equal(model.IntProperty, cloned.IntProperty); - Assert.Equal(model.StringProperty, cloned.StringProperty); - Assert.Equal(model.ListProperty, cloned.ListProperty); - var cdm = ((JToken)cloned.ObjectProperty).ToObject(); - Assert.Equal(((CloneModel)model.ObjectProperty).IntProperty, cdm.IntProperty); - } + var cloned = deserialized.DeepClone(); + Assert.Equal(model.IntProperty, cloned.IntProperty); + Assert.Equal(model.StringProperty, cloned.StringProperty); + Assert.Equal(model.ListProperty, cloned.ListProperty); + var cdm = ((JToken)cloned.ObjectProperty).ToObject(); + Assert.Equal(((CloneModel)model.ObjectProperty).IntProperty, cdm.IntProperty); + } - [Fact] - public void CanCloneMessagePackSerializedModel() + [Fact] + public void CanCloneMessagePackSerializedModel() + { + var model = new CloneModel { - var model = new CloneModel - { - IntProperty = 1, - StringProperty = "test", - ListProperty = new List { 1 }, - ObjectProperty = new CloneModel { IntProperty = 1 } - }; + IntProperty = 1, + StringProperty = "test", + ListProperty = new List { 1 }, + ObjectProperty = new CloneModel { IntProperty = 1 } + }; - var serializer = new MessagePackSerializer(); - var result = serializer.SerializeToBytes(model); - var deserialized = serializer.Deserialize(result); - Assert.Equal(model.IntProperty, deserialized.IntProperty); - Assert.Equal(model.StringProperty, deserialized.StringProperty); - Assert.Equal(model.ListProperty, deserialized.ListProperty); - var dm = (Dictionary)deserialized.ObjectProperty; - Assert.Equal(((CloneModel)model.ObjectProperty).IntProperty, Convert.ToInt32(dm["IntProperty"])); + var serializer = new MessagePackSerializer(); + var result = serializer.SerializeToBytes(model); + var deserialized = serializer.Deserialize(result); + Assert.Equal(model.IntProperty, deserialized.IntProperty); + Assert.Equal(model.StringProperty, deserialized.StringProperty); + Assert.Equal(model.ListProperty, deserialized.ListProperty); + var dm = (Dictionary)deserialized.ObjectProperty; + Assert.Equal(((CloneModel)model.ObjectProperty).IntProperty, Convert.ToInt32(dm["IntProperty"])); - var cloned = deserialized.DeepClone(); - Assert.Equal(model.IntProperty, cloned.IntProperty); - Assert.Equal(model.StringProperty, cloned.StringProperty); - Assert.Equal(model.ListProperty, cloned.ListProperty); - var cdm = (Dictionary)cloned.ObjectProperty; - Assert.Equal(((CloneModel)model.ObjectProperty).IntProperty, Convert.ToInt32(cdm["IntProperty"])); - } + var cloned = deserialized.DeepClone(); + Assert.Equal(model.IntProperty, cloned.IntProperty); + Assert.Equal(model.StringProperty, cloned.StringProperty); + Assert.Equal(model.ListProperty, cloned.ListProperty); + var cdm = (Dictionary)cloned.ObjectProperty; + Assert.Equal(((CloneModel)model.ObjectProperty).IntProperty, Convert.ToInt32(cdm["IntProperty"])); } +} - public class CloneModel - { - public int IntProperty { get; set; } - public string StringProperty { get; set; } - public List ListProperty { get; set; } - public IList EmptyStringList { get; } = new List(); - public ISet EmptyHashSet { get; } = new HashSet(); - public ISet HashSet { get; set; } - public object ObjectProperty { get; set; } - } +public class CloneModel +{ + public int IntProperty { get; set; } + public string StringProperty { get; set; } + public List ListProperty { get; set; } + public IList EmptyStringList { get; } = new List(); + public ISet EmptyHashSet { get; } = new HashSet(); + public ISet HashSet { get; set; } + public object ObjectProperty { get; set; } } diff --git a/tests/Foundatio.Tests/Utility/ConnectionStringParserTests.cs b/tests/Foundatio.Tests/Utility/ConnectionStringParserTests.cs index 4a3ec6a00..591ecb303 100644 --- a/tests/Foundatio.Tests/Utility/ConnectionStringParserTests.cs +++ b/tests/Foundatio.Tests/Utility/ConnectionStringParserTests.cs @@ -2,55 +2,54 @@ using Foundatio.Utility; using Xunit; -namespace Foundatio.Tests.Utility +namespace Foundatio.Tests.Utility; + +public class ConfigurationTests { - public class ConfigurationTests + [Fact] + public void CanParseConnectionString() + { + const string connectionString = "provider=azurestorage;DefaultEndpointsProtocol=https;AccountName=test;AccountKey=nx4TKwaaaaaaaaaa8t51oPyOErc/4N0TOjrMy6aaaaaabDMbFiK+Gf5rLr6XnU1aaaaaqiX2Yik7tvLcwp4lw==;EndpointSuffix=core.windows.net"; + var data = connectionString.ParseConnectionString(); + Assert.Equal(5, data.Count); + Assert.Equal("azurestorage", data["provider"]); + Assert.Equal("https", data["DefaultEndpointsProtocol"]); + Assert.Equal("test", data["AccountName"]); + Assert.Equal("nx4TKwaaaaaaaaaa8t51oPyOErc/4N0TOjrMy6aaaaaabDMbFiK+Gf5rLr6XnU1aaaaaqiX2Yik7tvLcwp4lw==", data["AccountKey"]); + Assert.Equal("core.windows.net", data["EndpointSuffix"]); + + Assert.Equal(connectionString, data.BuildConnectionString()); + } + + [Fact] + public void WillThrowOnInvalidConnectionStrings() { - [Fact] - public void CanParseConnectionString() - { - const string connectionString = "provider=azurestorage;DefaultEndpointsProtocol=https;AccountName=test;AccountKey=nx4TKwaaaaaaaaaa8t51oPyOErc/4N0TOjrMy6aaaaaabDMbFiK+Gf5rLr6XnU1aaaaaqiX2Yik7tvLcwp4lw==;EndpointSuffix=core.windows.net"; - var data = connectionString.ParseConnectionString(); - Assert.Equal(5, data.Count); - Assert.Equal("azurestorage", data["provider"]); - Assert.Equal("https", data["DefaultEndpointsProtocol"]); - Assert.Equal("test", data["AccountName"]); - Assert.Equal("nx4TKwaaaaaaaaaa8t51oPyOErc/4N0TOjrMy6aaaaaabDMbFiK+Gf5rLr6XnU1aaaaaqiX2Yik7tvLcwp4lw==", data["AccountKey"]); - Assert.Equal("core.windows.net", data["EndpointSuffix"]); - - Assert.Equal(connectionString, data.BuildConnectionString()); - } - - [Fact] - public void WillThrowOnInvalidConnectionStrings() - { - string connectionString = "provider = azurestorage; = ; DefaultEndpointsProtocol = https ;"; - Assert.Throws(() => connectionString.ParseConnectionString()); - - connectionString = "http://localhost:9200"; - Assert.Throws(() => connectionString.ParseConnectionString()); - } - - [Fact] - public void CanParseQuotedConnectionString() - { - const string connectionString = "Blah=\"Hey \"\"now\"\" stuff\""; - var data = connectionString.ParseConnectionString(); - Assert.Single(data); - Assert.Equal("Hey \"now\" stuff", data["blah"]); - - Assert.Equal(connectionString, data.BuildConnectionString()); - } - - [Fact] - public void CanParseComplexQuotedConnectionString() - { - const string connectionString = "Blah=\"foo1=\"\"my value\"\";foo2 =\"\"my value\"\";\""; - var data = connectionString.ParseConnectionString(); - Assert.Single(data); - Assert.Equal("foo1=\"my value\";foo2 =\"my value\";", data["Blah"]); - - Assert.Equal(connectionString, data.BuildConnectionString()); - } + string connectionString = "provider = azurestorage; = ; DefaultEndpointsProtocol = https ;"; + Assert.Throws(() => connectionString.ParseConnectionString()); + + connectionString = "http://localhost:9200"; + Assert.Throws(() => connectionString.ParseConnectionString()); + } + + [Fact] + public void CanParseQuotedConnectionString() + { + const string connectionString = "Blah=\"Hey \"\"now\"\" stuff\""; + var data = connectionString.ParseConnectionString(); + Assert.Single(data); + Assert.Equal("Hey \"now\" stuff", data["blah"]); + + Assert.Equal(connectionString, data.BuildConnectionString()); + } + + [Fact] + public void CanParseComplexQuotedConnectionString() + { + const string connectionString = "Blah=\"foo1=\"\"my value\"\";foo2 =\"\"my value\"\";\""; + var data = connectionString.ParseConnectionString(); + Assert.Single(data); + Assert.Equal("foo1=\"my value\";foo2 =\"my value\";", data["Blah"]); + + Assert.Equal(connectionString, data.BuildConnectionString()); } } diff --git a/tests/Foundatio.Tests/Utility/DataDictionaryTests.cs b/tests/Foundatio.Tests/Utility/DataDictionaryTests.cs index 694a1ff03..96deaa5ef 100644 --- a/tests/Foundatio.Tests/Utility/DataDictionaryTests.cs +++ b/tests/Foundatio.Tests/Utility/DataDictionaryTests.cs @@ -8,84 +8,83 @@ using Xunit; using Xunit.Abstractions; -namespace Foundatio.Tests.Utility +namespace Foundatio.Tests.Utility; + +public class DataDictionaryTests : TestWithLoggingBase { - public class DataDictionaryTests : TestWithLoggingBase - { - public DataDictionaryTests(ITestOutputHelper output) : base(output) { } - - [Fact] - public void CanGetData() - { - var serializer = new SystemTextJsonSerializer(); - - var model = new MyModel(); - var old = new MyDataModel { IntProperty = 12, StringProperty = "Kelly" }; - model.Data["Old"] = old; - model.Data["OldJson"] = JsonSerializer.Serialize(old); - model.Data["OldBytes"] = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(old)); - model.Data["New"] = new MyDataModel { IntProperty = 17, StringProperty = "Allen" }; - model.Data["Int16"] = (short)12; - model.Data["Int32"] = 12; - model.Data["Int64"] = 12L; - model.Data["bool"] = true; - - var dataOld = model.GetDataOrDefault("Old"); - Assert.Same(old, dataOld); - - Assert.Throws(() => model.GetDataOrDefault("OldJson")); - - var jsonDataOld = model.GetDataOrDefault("OldJson", serializer: serializer); - Assert.Equal(12, jsonDataOld.IntProperty); - Assert.Equal("Kelly", jsonDataOld.StringProperty); - - var bytesDataOld = model.GetDataOrDefault("OldBytes", serializer: serializer); - Assert.Equal(12, bytesDataOld.IntProperty); - Assert.Equal("Kelly", bytesDataOld.StringProperty); - - model.Serializer = serializer; - - jsonDataOld = model.GetDataOrDefault("OldJson"); - Assert.Equal(12, jsonDataOld.IntProperty); - Assert.Equal("Kelly", jsonDataOld.StringProperty); - - Assert.True(model.TryGetData("OldJson", out jsonDataOld)); - Assert.Equal(12, jsonDataOld.IntProperty); - Assert.Equal("Kelly", jsonDataOld.StringProperty); - Assert.False(model.TryGetData("OldJson2", out jsonDataOld)); - - bytesDataOld = model.GetDataOrDefault("OldBytes"); - Assert.Equal(12, bytesDataOld.IntProperty); - Assert.Equal("Kelly", bytesDataOld.StringProperty); - - Assert.Equal(12, model.GetDataOrDefault("Int16")); - Assert.Equal(12, model.GetDataOrDefault("Int32")); - Assert.Equal(12, model.GetDataOrDefault("Int64")); - - Assert.Equal(12, model.GetDataOrDefault("Int16")); - Assert.Equal(12, model.GetDataOrDefault("Int32")); - Assert.Equal(12, model.GetDataOrDefault("Int64")); - - Assert.Equal(12, model.GetDataOrDefault("Int16")); - Assert.Equal(12, model.GetDataOrDefault("Int32")); - Assert.Equal(12, model.GetDataOrDefault("Int64")); - - Assert.Equal(1, model.GetDataOrDefault("bool")); - } - } + public DataDictionaryTests(ITestOutputHelper output) : base(output) { } - public class MyModel : IHaveData, IHaveSerializer + [Fact] + public void CanGetData() { - public int IntProperty { get; set; } - public string StringProperty { get; set; } - public IDictionary Data { get; } = new DataDictionary(); + var serializer = new SystemTextJsonSerializer(); - public ISerializer Serializer { get; set; } - } + var model = new MyModel(); + var old = new MyDataModel { IntProperty = 12, StringProperty = "Kelly" }; + model.Data["Old"] = old; + model.Data["OldJson"] = JsonSerializer.Serialize(old); + model.Data["OldBytes"] = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(old)); + model.Data["New"] = new MyDataModel { IntProperty = 17, StringProperty = "Allen" }; + model.Data["Int16"] = (short)12; + model.Data["Int32"] = 12; + model.Data["Int64"] = 12L; + model.Data["bool"] = true; - public class MyDataModel - { - public int IntProperty { get; set; } - public string StringProperty { get; set; } + var dataOld = model.GetDataOrDefault("Old"); + Assert.Same(old, dataOld); + + Assert.Throws(() => model.GetDataOrDefault("OldJson")); + + var jsonDataOld = model.GetDataOrDefault("OldJson", serializer: serializer); + Assert.Equal(12, jsonDataOld.IntProperty); + Assert.Equal("Kelly", jsonDataOld.StringProperty); + + var bytesDataOld = model.GetDataOrDefault("OldBytes", serializer: serializer); + Assert.Equal(12, bytesDataOld.IntProperty); + Assert.Equal("Kelly", bytesDataOld.StringProperty); + + model.Serializer = serializer; + + jsonDataOld = model.GetDataOrDefault("OldJson"); + Assert.Equal(12, jsonDataOld.IntProperty); + Assert.Equal("Kelly", jsonDataOld.StringProperty); + + Assert.True(model.TryGetData("OldJson", out jsonDataOld)); + Assert.Equal(12, jsonDataOld.IntProperty); + Assert.Equal("Kelly", jsonDataOld.StringProperty); + Assert.False(model.TryGetData("OldJson2", out jsonDataOld)); + + bytesDataOld = model.GetDataOrDefault("OldBytes"); + Assert.Equal(12, bytesDataOld.IntProperty); + Assert.Equal("Kelly", bytesDataOld.StringProperty); + + Assert.Equal(12, model.GetDataOrDefault("Int16")); + Assert.Equal(12, model.GetDataOrDefault("Int32")); + Assert.Equal(12, model.GetDataOrDefault("Int64")); + + Assert.Equal(12, model.GetDataOrDefault("Int16")); + Assert.Equal(12, model.GetDataOrDefault("Int32")); + Assert.Equal(12, model.GetDataOrDefault("Int64")); + + Assert.Equal(12, model.GetDataOrDefault("Int16")); + Assert.Equal(12, model.GetDataOrDefault("Int32")); + Assert.Equal(12, model.GetDataOrDefault("Int64")); + + Assert.Equal(1, model.GetDataOrDefault("bool")); } } + +public class MyModel : IHaveData, IHaveSerializer +{ + public int IntProperty { get; set; } + public string StringProperty { get; set; } + public IDictionary Data { get; } = new DataDictionary(); + + public ISerializer Serializer { get; set; } +} + +public class MyDataModel +{ + public int IntProperty { get; set; } + public string StringProperty { get; set; } +} diff --git a/tests/Foundatio.Tests/Utility/RunTests.cs b/tests/Foundatio.Tests/Utility/RunTests.cs index dc5f9412b..fa47098ad 100644 --- a/tests/Foundatio.Tests/Utility/RunTests.cs +++ b/tests/Foundatio.Tests/Utility/RunTests.cs @@ -7,161 +7,160 @@ using Xunit; using Xunit.Abstractions; -namespace Foundatio.Tests.Utility +namespace Foundatio.Tests.Utility; + +public class RunTests : TestWithLoggingBase { - public class RunTests : TestWithLoggingBase - { - public RunTests(ITestOutputHelper output) : base(output) { } + public RunTests(ITestOutputHelper output) : base(output) { } - [Fact] - public async Task CanRunWithRetries() + [Fact] + public async Task CanRunWithRetries() + { + var task = Task.Run(() => { - var task = Task.Run(() => - { - _logger.LogInformation("Hi"); - }); + _logger.LogInformation("Hi"); + }); - await task; - await task; + await task; + await task; - await Run.WithRetriesAsync(() => - { - return DoStuff(); - }, maxAttempts: 5, retryInterval: TimeSpan.FromMilliseconds(10), cancellationToken: CancellationToken.None, logger: _logger); + await Run.WithRetriesAsync(() => + { + return DoStuff(); + }, maxAttempts: 5, retryInterval: TimeSpan.FromMilliseconds(10), cancellationToken: CancellationToken.None, logger: _logger); - await Run.WithRetriesAsync(async () => - { - await DoStuff(); - }, maxAttempts: 5, retryInterval: TimeSpan.FromMilliseconds(10), cancellationToken: CancellationToken.None, logger: _logger); - } + await Run.WithRetriesAsync(async () => + { + await DoStuff(); + }, maxAttempts: 5, retryInterval: TimeSpan.FromMilliseconds(10), cancellationToken: CancellationToken.None, logger: _logger); + } - [Fact] - public async Task CanRunWithRetriesAndResult() + [Fact] + public async Task CanRunWithRetriesAndResult() + { + var result = await Run.WithRetriesAsync(() => { - var result = await Run.WithRetriesAsync(() => - { - return ReturnStuff(); - }, maxAttempts: 5, retryInterval: TimeSpan.FromMilliseconds(10), cancellationToken: CancellationToken.None, logger: _logger); + return ReturnStuff(); + }, maxAttempts: 5, retryInterval: TimeSpan.FromMilliseconds(10), cancellationToken: CancellationToken.None, logger: _logger); - Assert.Equal(1, result); + Assert.Equal(1, result); - result = await Run.WithRetriesAsync(async () => - { - return await ReturnStuff(); - }, maxAttempts: 5, retryInterval: TimeSpan.FromMilliseconds(10), cancellationToken: CancellationToken.None, logger: _logger); + result = await Run.WithRetriesAsync(async () => + { + return await ReturnStuff(); + }, maxAttempts: 5, retryInterval: TimeSpan.FromMilliseconds(10), cancellationToken: CancellationToken.None, logger: _logger); - Assert.Equal(1, result); - } + Assert.Equal(1, result); + } - [Fact] - public async Task CanBoomWithRetries() + [Fact] + public async Task CanBoomWithRetries() + { + var exception = await Assert.ThrowsAsync(async () => { - var exception = await Assert.ThrowsAsync(async () => - { - await Run.WithRetriesAsync(() => - { - return DoBoom(); - }, maxAttempts: 5, retryInterval: TimeSpan.FromMilliseconds(10), cancellationToken: CancellationToken.None, logger: _logger); - }); - Assert.Equal("Hi", exception.Message); - - exception = await Assert.ThrowsAsync(async () => - { - await Run.WithRetriesAsync(async () => - { - await DoBoom(); - }, maxAttempts: 5, retryInterval: TimeSpan.FromMilliseconds(10), cancellationToken: CancellationToken.None, logger: _logger); - }); - Assert.Equal("Hi", exception.Message); - - int attempt = 0; await Run.WithRetriesAsync(() => { - attempt++; - return DoBoom(attempt < 5); + return DoBoom(); }, maxAttempts: 5, retryInterval: TimeSpan.FromMilliseconds(10), cancellationToken: CancellationToken.None, logger: _logger); - Assert.Equal(5, attempt); + }); + Assert.Equal("Hi", exception.Message); - attempt = 0; + exception = await Assert.ThrowsAsync(async () => + { await Run.WithRetriesAsync(async () => { - attempt++; - await DoBoom(attempt < 5); + await DoBoom(); }, maxAttempts: 5, retryInterval: TimeSpan.FromMilliseconds(10), cancellationToken: CancellationToken.None, logger: _logger); - Assert.Equal(5, attempt); - } + }); + Assert.Equal("Hi", exception.Message); - [Fact] - public async Task CanBoomWithRetriesAndResult() + int attempt = 0; + await Run.WithRetriesAsync(() => { - var exception = await Assert.ThrowsAsync(async () => - { - var result = await Run.WithRetriesAsync(() => - { - return ReturnBoom(); - }, maxAttempts: 5, retryInterval: TimeSpan.FromMilliseconds(10), cancellationToken: CancellationToken.None, logger: _logger); + attempt++; + return DoBoom(attempt < 5); + }, maxAttempts: 5, retryInterval: TimeSpan.FromMilliseconds(10), cancellationToken: CancellationToken.None, logger: _logger); + Assert.Equal(5, attempt); - Assert.Equal(1, result); - }); - Assert.Equal("Hi", exception.Message); - - exception = await Assert.ThrowsAsync(async () => - { - var result = await Run.WithRetriesAsync(async () => - { - return await ReturnBoom(); - }, maxAttempts: 5, retryInterval: TimeSpan.FromMilliseconds(10), cancellationToken: CancellationToken.None, logger: _logger); - - Assert.Equal(1, result); - }); - Assert.Equal("Hi", exception.Message); + attempt = 0; + await Run.WithRetriesAsync(async () => + { + attempt++; + await DoBoom(attempt < 5); + }, maxAttempts: 5, retryInterval: TimeSpan.FromMilliseconds(10), cancellationToken: CancellationToken.None, logger: _logger); + Assert.Equal(5, attempt); + } - int attempt = 0; + [Fact] + public async Task CanBoomWithRetriesAndResult() + { + var exception = await Assert.ThrowsAsync(async () => + { var result = await Run.WithRetriesAsync(() => { - attempt++; - return ReturnBoom(attempt < 5); + return ReturnBoom(); }, maxAttempts: 5, retryInterval: TimeSpan.FromMilliseconds(10), cancellationToken: CancellationToken.None, logger: _logger); - Assert.Equal(5, attempt); + Assert.Equal(1, result); + }); + Assert.Equal("Hi", exception.Message); - attempt = 0; - result = await Run.WithRetriesAsync(async () => + exception = await Assert.ThrowsAsync(async () => + { + var result = await Run.WithRetriesAsync(async () => { - attempt++; - return await ReturnBoom(attempt < 5); + return await ReturnBoom(); }, maxAttempts: 5, retryInterval: TimeSpan.FromMilliseconds(10), cancellationToken: CancellationToken.None, logger: _logger); - Assert.Equal(5, attempt); + Assert.Equal(1, result); - } + }); + Assert.Equal("Hi", exception.Message); - private async Task ReturnStuff() + int attempt = 0; + var result = await Run.WithRetriesAsync(() => { - await Task.Delay(10); - return 1; - } - - private Task DoStuff() + attempt++; + return ReturnBoom(attempt < 5); + }, maxAttempts: 5, retryInterval: TimeSpan.FromMilliseconds(10), cancellationToken: CancellationToken.None, logger: _logger); + Assert.Equal(5, attempt); + Assert.Equal(1, result); + + attempt = 0; + result = await Run.WithRetriesAsync(async () => { - return Task.Delay(10); - } + attempt++; + return await ReturnBoom(attempt < 5); + }, maxAttempts: 5, retryInterval: TimeSpan.FromMilliseconds(10), cancellationToken: CancellationToken.None, logger: _logger); + Assert.Equal(5, attempt); + Assert.Equal(1, result); + } - private async Task ReturnBoom(bool shouldThrow = true) - { - await Task.Delay(10); + private async Task ReturnStuff() + { + await Task.Delay(10); + return 1; + } + + private Task DoStuff() + { + return Task.Delay(10); + } - if (shouldThrow) - throw new ApplicationException("Hi"); + private async Task ReturnBoom(bool shouldThrow = true) + { + await Task.Delay(10); - return 1; - } + if (shouldThrow) + throw new ApplicationException("Hi"); - private async Task DoBoom(bool shouldThrow = true) - { - await Task.Delay(10); + return 1; + } + + private async Task DoBoom(bool shouldThrow = true) + { + await Task.Delay(10); - if (shouldThrow) - throw new ApplicationException("Hi"); - } + if (shouldThrow) + throw new ApplicationException("Hi"); } } diff --git a/tests/Foundatio.Tests/Utility/ScheduledTimerTests.cs b/tests/Foundatio.Tests/Utility/ScheduledTimerTests.cs index 1844c8ec1..09d7b0ec0 100644 --- a/tests/Foundatio.Tests/Utility/ScheduledTimerTests.cs +++ b/tests/Foundatio.Tests/Utility/ScheduledTimerTests.cs @@ -9,101 +9,100 @@ using Xunit; using Xunit.Abstractions; -namespace Foundatio.Tests.Utility +namespace Foundatio.Tests.Utility; + +public class ScheduledTimerTests : TestWithLoggingBase { - public class ScheduledTimerTests : TestWithLoggingBase + public ScheduledTimerTests(ITestOutputHelper output) : base(output) + { + Log.SetLogLevel(LogLevel.Trace); + } + + [Fact] + public Task CanRun() { - public ScheduledTimerTests(ITestOutputHelper output) : base(output) + var resetEvent = new AsyncAutoResetEvent(); + Task Callback() { - Log.SetLogLevel(LogLevel.Trace); + resetEvent.Set(); + return null; } - [Fact] - public Task CanRun() - { - var resetEvent = new AsyncAutoResetEvent(); - Task Callback() - { - resetEvent.Set(); - return null; - } + using var timer = new ScheduledTimer(Callback, loggerFactory: Log); + timer.ScheduleNext(); + return resetEvent.WaitAsync(new CancellationTokenSource(500).Token); + } - using var timer = new ScheduledTimer(Callback, loggerFactory: Log); - timer.ScheduleNext(); - return resetEvent.WaitAsync(new CancellationTokenSource(500).Token); - } + [RetryFact] + public Task CanRunAndScheduleConcurrently() + { + return CanRunConcurrentlyAsync(); + } - [RetryFact] - public Task CanRunAndScheduleConcurrently() - { - return CanRunConcurrentlyAsync(); - } + [Fact] + public Task CanRunWithMinimumInterval() + { + return CanRunConcurrentlyAsync(TimeSpan.FromMilliseconds(100)); + } + + private async Task CanRunConcurrentlyAsync(TimeSpan? minimumIntervalTime = null) + { + Log.MinimumLevel = LogLevel.Trace; + const int iterations = 2; + var countdown = new AsyncCountdownEvent(iterations); - [Fact] - public Task CanRunWithMinimumInterval() + async Task Callback() { - return CanRunConcurrentlyAsync(TimeSpan.FromMilliseconds(100)); + _logger.LogInformation("Starting work"); + await SystemClock.SleepAsync(250); + countdown.Signal(); + _logger.LogInformation("Finished work"); + return null; } - private async Task CanRunConcurrentlyAsync(TimeSpan? minimumIntervalTime = null) + using var timer = new ScheduledTimer(Callback, minimumIntervalTime: minimumIntervalTime, loggerFactory: Log); + timer.ScheduleNext(); + _ = Task.Run(async () => { - Log.MinimumLevel = LogLevel.Trace; - const int iterations = 2; - var countdown = new AsyncCountdownEvent(iterations); - - async Task Callback() + for (int i = 0; i < iterations; i++) { - _logger.LogInformation("Starting work"); - await SystemClock.SleepAsync(250); - countdown.Signal(); - _logger.LogInformation("Finished work"); - return null; + await SystemClock.SleepAsync(10); + timer.ScheduleNext(); } + }); - using var timer = new ScheduledTimer(Callback, minimumIntervalTime: minimumIntervalTime, loggerFactory: Log); - timer.ScheduleNext(); - _ = Task.Run(async () => - { - for (int i = 0; i < iterations; i++) - { - await SystemClock.SleepAsync(10); - timer.ScheduleNext(); - } - }); + _logger.LogInformation("Waiting for 300ms"); + await countdown.WaitAsync(TimeSpan.FromMilliseconds(300)); + _logger.LogInformation("Finished waiting for 300ms"); + Assert.Equal(iterations - 1, countdown.CurrentCount); - _logger.LogInformation("Waiting for 300ms"); - await countdown.WaitAsync(TimeSpan.FromMilliseconds(300)); - _logger.LogInformation("Finished waiting for 300ms"); - Assert.Equal(iterations - 1, countdown.CurrentCount); + _logger.LogInformation("Waiting for 1.5 seconds"); + await countdown.WaitAsync(TimeSpan.FromSeconds(1.5)); + _logger.LogInformation("Finished waiting for 1.5 seconds"); + Assert.Equal(0, countdown.CurrentCount); + } - _logger.LogInformation("Waiting for 1.5 seconds"); - await countdown.WaitAsync(TimeSpan.FromSeconds(1.5)); - _logger.LogInformation("Finished waiting for 1.5 seconds"); - Assert.Equal(0, countdown.CurrentCount); - } + [Fact] + public async Task CanRecoverFromError() + { + int hits = 0; + var resetEvent = new AsyncAutoResetEvent(false); - [Fact] - public async Task CanRecoverFromError() + Task Callback() { - int hits = 0; - var resetEvent = new AsyncAutoResetEvent(false); - - Task Callback() - { - Interlocked.Increment(ref hits); - if (_logger.IsEnabled(LogLevel.Information)) - _logger.LogInformation("Callback called for the #{Hits} time", hits); - if (hits == 1) - throw new Exception("Error in callback"); + Interlocked.Increment(ref hits); + if (_logger.IsEnabled(LogLevel.Information)) + _logger.LogInformation("Callback called for the #{Hits} time", hits); + if (hits == 1) + throw new Exception("Error in callback"); - resetEvent.Set(); - return Task.FromResult(null); - } - - using var timer = new ScheduledTimer(Callback, loggerFactory: Log); - timer.ScheduleNext(); - await resetEvent.WaitAsync(new CancellationTokenSource(800).Token); - Assert.Equal(2, hits); + resetEvent.Set(); + return Task.FromResult(null); } + + using var timer = new ScheduledTimer(Callback, loggerFactory: Log); + timer.ScheduleNext(); + await resetEvent.WaitAsync(new CancellationTokenSource(800).Token); + Assert.Equal(2, hits); } } diff --git a/tests/Foundatio.Tests/Utility/SystemClockTests.cs b/tests/Foundatio.Tests/Utility/SystemClockTests.cs index 7e551f0fa..dd14b6563 100644 --- a/tests/Foundatio.Tests/Utility/SystemClockTests.cs +++ b/tests/Foundatio.Tests/Utility/SystemClockTests.cs @@ -6,126 +6,125 @@ using Xunit; using Xunit.Abstractions; -namespace Foundatio.Tests.Utility +namespace Foundatio.Tests.Utility; + +public class SystemClockTests : TestWithLoggingBase { - public class SystemClockTests : TestWithLoggingBase - { - public SystemClockTests(ITestOutputHelper output) : base(output) { } + public SystemClockTests(ITestOutputHelper output) : base(output) { } - [Fact] - public void CanGetTime() + [Fact] + public void CanGetTime() + { + using (TestSystemClock.Install()) { - using (TestSystemClock.Install()) - { - var now = DateTime.UtcNow; - TestSystemClock.SetFrozenTime(now); - Assert.Equal(now, SystemClock.UtcNow); - Assert.Equal(now.ToLocalTime(), SystemClock.Now); - Assert.Equal(now, SystemClock.OffsetUtcNow); - Assert.Equal(now.ToLocalTime(), SystemClock.OffsetNow); - Assert.Equal(DateTimeOffset.Now.Offset, SystemClock.TimeZoneOffset); - } + var now = DateTime.UtcNow; + TestSystemClock.SetFrozenTime(now); + Assert.Equal(now, SystemClock.UtcNow); + Assert.Equal(now.ToLocalTime(), SystemClock.Now); + Assert.Equal(now, SystemClock.OffsetUtcNow); + Assert.Equal(now.ToLocalTime(), SystemClock.OffsetNow); + Assert.Equal(DateTimeOffset.Now.Offset, SystemClock.TimeZoneOffset); } + } - [Fact] - public void CanSleep() + [Fact] + public void CanSleep() + { + using (TestSystemClock.Install()) { - using (TestSystemClock.Install()) - { - var sw = Stopwatch.StartNew(); - SystemClock.Sleep(250); - sw.Stop(); - Assert.InRange(sw.ElapsedMilliseconds, 225, 400); - - TestSystemClock.UseFakeSleep(); - - var now = SystemClock.UtcNow; - sw.Restart(); - SystemClock.Sleep(1000); - sw.Stop(); - var afterSleepNow = SystemClock.UtcNow; - - Assert.InRange(sw.ElapsedMilliseconds, 0, 30); - Assert.True(afterSleepNow > now); - Assert.InRange(afterSleepNow.Subtract(now).TotalMilliseconds, 950, 1100); - } + var sw = Stopwatch.StartNew(); + SystemClock.Sleep(250); + sw.Stop(); + Assert.InRange(sw.ElapsedMilliseconds, 225, 400); + + TestSystemClock.UseFakeSleep(); + + var now = SystemClock.UtcNow; + sw.Restart(); + SystemClock.Sleep(1000); + sw.Stop(); + var afterSleepNow = SystemClock.UtcNow; + + Assert.InRange(sw.ElapsedMilliseconds, 0, 30); + Assert.True(afterSleepNow > now); + Assert.InRange(afterSleepNow.Subtract(now).TotalMilliseconds, 950, 1100); } + } - [Fact] - public async Task CanSleepAsync() + [Fact] + public async Task CanSleepAsync() + { + using (TestSystemClock.Install()) { - using (TestSystemClock.Install()) - { - var sw = Stopwatch.StartNew(); - await SystemClock.SleepAsync(250); - sw.Stop(); - - Assert.InRange(sw.ElapsedMilliseconds, 225, 3000); - - TestSystemClock.UseFakeSleep(); - - var now = SystemClock.UtcNow; - sw.Restart(); - await SystemClock.SleepAsync(1000); - sw.Stop(); - var afterSleepNow = SystemClock.UtcNow; - - Assert.InRange(sw.ElapsedMilliseconds, 0, 30); - Assert.True(afterSleepNow > now); - Assert.InRange(afterSleepNow.Subtract(now).TotalMilliseconds, 950, 5000); - } + var sw = Stopwatch.StartNew(); + await SystemClock.SleepAsync(250); + sw.Stop(); + + Assert.InRange(sw.ElapsedMilliseconds, 225, 3000); + + TestSystemClock.UseFakeSleep(); + + var now = SystemClock.UtcNow; + sw.Restart(); + await SystemClock.SleepAsync(1000); + sw.Stop(); + var afterSleepNow = SystemClock.UtcNow; + + Assert.InRange(sw.ElapsedMilliseconds, 0, 30); + Assert.True(afterSleepNow > now); + Assert.InRange(afterSleepNow.Subtract(now).TotalMilliseconds, 950, 5000); } + } - [Fact] - public void CanSetTimeZone() + [Fact] + public void CanSetTimeZone() + { + using (TestSystemClock.Install()) { - using (TestSystemClock.Install()) - { - var utcNow = DateTime.UtcNow; - var now = new DateTime(utcNow.AddHours(1).Ticks, DateTimeKind.Local); - TestSystemClock.SetFrozenTime(utcNow); - TestSystemClock.SetTimeZoneOffset(TimeSpan.FromHours(1)); - - Assert.Equal(utcNow, SystemClock.UtcNow); - Assert.Equal(utcNow, SystemClock.OffsetUtcNow); - Assert.Equal(now, SystemClock.Now); - Assert.Equal(new DateTimeOffset(now.Ticks, TimeSpan.FromHours(1)), SystemClock.OffsetNow); - Assert.Equal(TimeSpan.FromHours(1), SystemClock.TimeZoneOffset); - } + var utcNow = DateTime.UtcNow; + var now = new DateTime(utcNow.AddHours(1).Ticks, DateTimeKind.Local); + TestSystemClock.SetFrozenTime(utcNow); + TestSystemClock.SetTimeZoneOffset(TimeSpan.FromHours(1)); + + Assert.Equal(utcNow, SystemClock.UtcNow); + Assert.Equal(utcNow, SystemClock.OffsetUtcNow); + Assert.Equal(now, SystemClock.Now); + Assert.Equal(new DateTimeOffset(now.Ticks, TimeSpan.FromHours(1)), SystemClock.OffsetNow); + Assert.Equal(TimeSpan.FromHours(1), SystemClock.TimeZoneOffset); } + } - [Fact] - public void CanSetLocalFixedTime() + [Fact] + public void CanSetLocalFixedTime() + { + using (TestSystemClock.Install()) { - using (TestSystemClock.Install()) - { - var now = DateTime.Now; - var utcNow = now.ToUniversalTime(); - TestSystemClock.SetFrozenTime(now); - - Assert.Equal(now, SystemClock.Now); - Assert.Equal(now, SystemClock.OffsetNow); - Assert.Equal(utcNow, SystemClock.UtcNow); - Assert.Equal(utcNow, SystemClock.OffsetUtcNow); - Assert.Equal(DateTimeOffset.Now.Offset, SystemClock.TimeZoneOffset); - } + var now = DateTime.Now; + var utcNow = now.ToUniversalTime(); + TestSystemClock.SetFrozenTime(now); + + Assert.Equal(now, SystemClock.Now); + Assert.Equal(now, SystemClock.OffsetNow); + Assert.Equal(utcNow, SystemClock.UtcNow); + Assert.Equal(utcNow, SystemClock.OffsetUtcNow); + Assert.Equal(DateTimeOffset.Now.Offset, SystemClock.TimeZoneOffset); } + } - [Fact] - public void CanSetUtcFixedTime() + [Fact] + public void CanSetUtcFixedTime() + { + using (TestSystemClock.Install()) { - using (TestSystemClock.Install()) - { - var utcNow = DateTime.UtcNow; - var now = utcNow.ToLocalTime(); - TestSystemClock.SetFrozenTime(utcNow); - - Assert.Equal(now, SystemClock.Now); - Assert.Equal(now, SystemClock.OffsetNow); - Assert.Equal(utcNow, SystemClock.UtcNow); - Assert.Equal(utcNow, SystemClock.OffsetUtcNow); - Assert.Equal(DateTimeOffset.Now.Offset, SystemClock.TimeZoneOffset); - } + var utcNow = DateTime.UtcNow; + var now = utcNow.ToLocalTime(); + TestSystemClock.SetFrozenTime(utcNow); + + Assert.Equal(now, SystemClock.Now); + Assert.Equal(now, SystemClock.OffsetNow); + Assert.Equal(utcNow, SystemClock.UtcNow); + Assert.Equal(utcNow, SystemClock.OffsetUtcNow); + Assert.Equal(DateTimeOffset.Now.Offset, SystemClock.TimeZoneOffset); } } } diff --git a/tests/Foundatio.Tests/Utility/TestUdpListener.cs b/tests/Foundatio.Tests/Utility/TestUdpListener.cs index 2a03e6d4e..7c99fd7a0 100644 --- a/tests/Foundatio.Tests/Utility/TestUdpListener.cs +++ b/tests/Foundatio.Tests/Utility/TestUdpListener.cs @@ -7,145 +7,144 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; -namespace Foundatio.Tests.Utility +namespace Foundatio.Tests.Utility; + +public class TestUdpListener : IDisposable { - public class TestUdpListener : IDisposable + private readonly List _messages = new(); + private UdpClient _listener; + private readonly IPEndPoint _localIpEndPoint; + private IPEndPoint _senderIpEndPoint; + private readonly ILogger _logger; + private Task _receiveTask; + private CancellationTokenSource _cancellationTokenSource; + private readonly object _lock = new(); + + public TestUdpListener(string server, int port, ILoggerFactory loggerFactory) { - private readonly List _messages = new(); - private UdpClient _listener; - private readonly IPEndPoint _localIpEndPoint; - private IPEndPoint _senderIpEndPoint; - private readonly ILogger _logger; - private Task _receiveTask; - private CancellationTokenSource _cancellationTokenSource; - private readonly object _lock = new(); - - public TestUdpListener(string server, int port, ILoggerFactory loggerFactory) - { - _logger = loggerFactory.CreateLogger(); - _localIpEndPoint = new IPEndPoint(IPAddress.Parse(server), port); - _senderIpEndPoint = new IPEndPoint(IPAddress.Any, 0); - } + _logger = loggerFactory.CreateLogger(); + _localIpEndPoint = new IPEndPoint(IPAddress.Parse(server), port); + _senderIpEndPoint = new IPEndPoint(IPAddress.Any, 0); + } - public string[] GetMessages() - { - return _messages.ToArray(); - } + public string[] GetMessages() + { + return _messages.ToArray(); + } - public void ResetMessages() - { - _logger.LogInformation("ResetMessages"); - _messages.Clear(); - } + public void ResetMessages() + { + _logger.LogInformation("ResetMessages"); + _messages.Clear(); + } - public void StartListening() + public void StartListening() + { + lock (_lock) { - lock (_lock) + if (_listener != null) { - if (_listener != null) - { - _logger.LogInformation("StartListening: Already listening"); - return; - } - - _logger.LogInformation("StartListening"); - _cancellationTokenSource = new CancellationTokenSource(); - _listener = new UdpClient(_localIpEndPoint) - { - Client = { ReceiveTimeout = 1000 } - }; - _receiveTask = Task.Factory.StartNew(() => ProcessMessages(_cancellationTokenSource.Token), _cancellationTokenSource.Token); + _logger.LogInformation("StartListening: Already listening"); + return; } + + _logger.LogInformation("StartListening"); + _cancellationTokenSource = new CancellationTokenSource(); + _listener = new UdpClient(_localIpEndPoint) + { + Client = { ReceiveTimeout = 1000 } + }; + _receiveTask = Task.Factory.StartNew(() => ProcessMessages(_cancellationTokenSource.Token), _cancellationTokenSource.Token); } + } - private void ProcessMessages(CancellationToken cancellationToken) + private void ProcessMessages(CancellationToken cancellationToken) + { + while (true) { - while (true) + if (cancellationToken.IsCancellationRequested) { - if (cancellationToken.IsCancellationRequested) - { - _logger.LogInformation("Stopped ProcessMessages due to CancellationToken.IsCancellationRequested"); - _cancellationTokenSource = null; - StopListening(); - return; - } - - try - { - lock (_lock) - { - if (_listener == null) - break; - - var result = _listener.Receive(ref _senderIpEndPoint); - if (result.Length == 0) - continue; - - string message = Encoding.UTF8.GetString(result, 0, result.Length); - _logger.LogInformation("Received message: {Message}", message); - _messages.Add(message); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error during ProcessMessages: {Message}", ex.Message); - } + _logger.LogInformation("Stopped ProcessMessages due to CancellationToken.IsCancellationRequested"); + _cancellationTokenSource = null; + StopListening(); + return; } - } - public void StopListening() - { try { lock (_lock) { - _logger.LogInformation("Closing listener socket"); - _listener?.Close(); - _listener?.Dispose(); - _listener = null; - _cancellationTokenSource?.Cancel(); - _receiveTask?.Wait(1000); - _receiveTask = null; + if (_listener == null) + break; + + var result = _listener.Receive(ref _senderIpEndPoint); + if (result.Length == 0) + continue; + + string message = Encoding.UTF8.GetString(result, 0, result.Length); + _logger.LogInformation("Received message: {Message}", message); + _messages.Add(message); } } catch (Exception ex) { - _logger.LogError(ex, "Error during StopListening: {Message}", ex.Message); + _logger.LogError(ex, "Error during ProcessMessages: {Message}", ex.Message); } } + } - public void StopListening(int expectedMessageCount) + public void StopListening() + { + try + { + lock (_lock) + { + _logger.LogInformation("Closing listener socket"); + _listener?.Close(); + _listener?.Dispose(); + _listener = null; + _cancellationTokenSource?.Cancel(); + _receiveTask?.Wait(1000); + _receiveTask = null; + } + } + catch (Exception ex) { - StopListening(expectedMessageCount, new CancellationTokenSource(10000).Token); + _logger.LogError(ex, "Error during StopListening: {Message}", ex.Message); } + } + + public void StopListening(int expectedMessageCount) + { + StopListening(expectedMessageCount, new CancellationTokenSource(10000).Token); + } - public void StopListening(int expectedMessageCount, CancellationToken cancellationToken) + public void StopListening(int expectedMessageCount, CancellationToken cancellationToken) + { + _logger.LogInformation($"StopListening called, waiting for {expectedMessageCount} expected message(s)"); + while (_messages.Count < expectedMessageCount) { - _logger.LogInformation($"StopListening called, waiting for {expectedMessageCount} expected message(s)"); - while (_messages.Count < expectedMessageCount) + if (cancellationToken.IsCancellationRequested) { - if (cancellationToken.IsCancellationRequested) - { - _logger.LogInformation("Stopped listening due to CancellationToken.IsCancellationRequested"); - break; - } - if (_cancellationTokenSource.Token.IsCancellationRequested) - { - _logger.LogInformation("Stopped listening due to CancellationTokenSource.IsCancellationRequested"); - break; - } - - Thread.Sleep(100); + _logger.LogInformation("Stopped listening due to CancellationToken.IsCancellationRequested"); + break; + } + if (_cancellationTokenSource.Token.IsCancellationRequested) + { + _logger.LogInformation("Stopped listening due to CancellationTokenSource.IsCancellationRequested"); + break; } - _logger.LogInformation("StopListening Count={Count}", _messages.Count); - StopListening(); + Thread.Sleep(100); } - public void Dispose() - { - _logger.LogInformation("Dispose"); - StopListening(); - } + _logger.LogInformation("StopListening Count={Count}", _messages.Count); + StopListening(); + } + + public void Dispose() + { + _logger.LogInformation("Dispose"); + StopListening(); } }