From 7a84bc11a58315f807a0d67a4d3cead9eff0904f Mon Sep 17 00:00:00 2001 From: Denys Kozhevnikov Date: Fri, 3 Apr 2020 10:12:27 +0300 Subject: [PATCH] - add ability to inject clock for 'UtcNow' resolving --- .../BackgroundJobServerHostedService.cs | 10 +- .../Dashboard/AspNetCoreDashboardContext.cs | 17 +- .../AspNetCoreDashboardMiddleware.cs | 24 +-- .../HangfireApplicationBuilderExtensions.cs | 28 +-- .../HangfireServiceCollectionExtensions.cs | 42 +++-- src/Hangfire.Core/AppBuilderExtensions.cs | 163 +++++++++--------- src/Hangfire.Core/BackgroundJobClient.cs | 67 +++---- src/Hangfire.Core/BackgroundJobServer.cs | 47 ++--- .../Client/CoreBackgroundJobFactory.cs | 17 +- src/Hangfire.Core/Client/CreateContext.cs | 49 +++--- .../ContinuationsSupportAttribute.cs | 27 +-- .../Dashboard/DashboardContext.cs | 25 +-- .../Dashboard/Owin/MiddlewareExtensions.cs | 30 ++-- .../Dashboard/Owin/OwinDashboardContext.cs | 17 +- .../Dashboard/RouteCollectionExtensions.cs | 30 ++-- src/Hangfire.Core/IClock.cs | 36 ++++ src/Hangfire.Core/LatencyTimeoutAttribute.cs | 10 +- .../BootstrapperConfigurationExtensions.cs | 24 +-- .../Obsolete/DashboardMiddleware.cs | 25 +-- src/Hangfire.Core/RecurringJobExtensions.cs | 21 ++- src/Hangfire.Core/RecurringJobManager.cs | 66 +++---- .../Server/BackgroundProcessContext.cs | 37 ++-- .../BackgroundProcessDispatcherBuilder.cs | 13 +- ...BackgroundProcessDispatcherBuilderAsync.cs | 21 +-- .../Server/BackgroundProcessingServer.cs | 32 ++-- .../Server/BackgroundServerContext.cs | 19 +- .../Server/BackgroundServerProcess.cs | 19 +- .../Server/DelayedJobScheduler.cs | 47 ++--- .../Server/RecurringJobScheduler.cs | 72 ++++---- src/Hangfire.Core/Server/Worker.cs | 48 +++--- src/Hangfire.Core/States/ApplyStateContext.cs | 52 +++--- src/Hangfire.Core/States/AwaitingState.cs | 28 +-- .../States/BackgroundJobStateChanger.cs | 31 ++-- src/Hangfire.Core/States/ElectStateContext.cs | 22 ++- .../States/StateChangeContext.cs | 43 +++-- .../StatisticsHistoryAttribute.cs | 24 +-- .../BackgroundJobClientFacts.cs | 22 ++- .../Client/BackgroundJobFactoryFacts.cs | 19 +- .../Client/ClientExceptionContextFacts.cs | 3 +- .../Client/CreateContextFacts.cs | 22 ++- .../Client/CreatedContextFacts.cs | 9 +- .../Client/CreatingContextFacts.cs | 9 +- .../LatencyTimeoutAttributeFacts.cs | 4 +- .../Mocks/ApplyStateContextMock.cs | 11 +- .../Mocks/BackgroundProcessContextMock.cs | 7 +- .../Mocks/CreateContextMock.cs | 5 +- .../Mocks/StateChangeContextMock.cs | 7 +- .../PreserveCultureAttributeFacts.cs | 15 +- .../RecurringJobManagerFacts.cs | 45 ++--- .../Server/BackgroundProcessContextFacts.cs | 22 ++- .../Server/BackgroundProcessingServerFacts.cs | 38 ++-- .../Server/DelayedJobSchedulerFacts.cs | 2 + .../Server/RecurringJobSchedulerFacts.cs | 42 ++--- .../States/ApplyStateContextFacts.cs | 23 ++- .../StatisticsHistoryAttributeFacts.cs | 2 + .../Stubs/DashboardContextStub.cs | 2 +- .../Stubs/JobStorageStub.cs | 4 + 57 files changed, 894 insertions(+), 702 deletions(-) create mode 100644 src/Hangfire.Core/IClock.cs diff --git a/src/Hangfire.AspNetCore/BackgroundJobServerHostedService.cs b/src/Hangfire.AspNetCore/BackgroundJobServerHostedService.cs index d60d16e02..b3eb01d1f 100644 --- a/src/Hangfire.AspNetCore/BackgroundJobServerHostedService.cs +++ b/src/Hangfire.AspNetCore/BackgroundJobServerHostedService.cs @@ -15,6 +15,7 @@ public class BackgroundJobServerHostedService : IHostedService, IDisposable { private readonly BackgroundJobServerOptions _options; private readonly JobStorage _storage; + private readonly IClock _clock; private readonly IEnumerable _additionalProcesses; private readonly IBackgroundJobFactory _factory; private readonly IBackgroundJobPerformer _performer; @@ -24,10 +25,11 @@ public class BackgroundJobServerHostedService : IHostedService, IDisposable public BackgroundJobServerHostedService( [NotNull] JobStorage storage, + [NotNull] IClock clock, [NotNull] BackgroundJobServerOptions options, [NotNull] IEnumerable additionalProcesses) #pragma warning disable 618 - : this(storage, options, additionalProcesses, null, null, null) + : this(storage, clock, options, additionalProcesses, null, null, null) #pragma warning restore 618 { } @@ -35,6 +37,7 @@ public BackgroundJobServerHostedService( [Obsolete("This constructor uses an obsolete constructor overload of the BackgroundJobServer type that will be removed in 2.0.0.")] public BackgroundJobServerHostedService( [NotNull] JobStorage storage, + [NotNull] IClock clock, [NotNull] BackgroundJobServerOptions options, [NotNull] IEnumerable additionalProcesses, [CanBeNull] IBackgroundJobFactory factory, @@ -43,6 +46,7 @@ public BackgroundJobServerHostedService( { _options = options ?? throw new ArgumentNullException(nameof(options)); _storage = storage ?? throw new ArgumentNullException(nameof(storage)); + _clock = clock ?? throw new ArgumentNullException(nameof(clock)); _additionalProcesses = additionalProcesses; @@ -55,9 +59,9 @@ public Task StartAsync(CancellationToken cancellationToken) { _processingServer = _factory != null && _performer != null && _stateChanger != null #pragma warning disable 618 - ? new BackgroundJobServer(_options, _storage, _additionalProcesses, null, null, _factory, _performer, _stateChanger) + ? new BackgroundJobServer(_options, _storage, _clock, _additionalProcesses, null, null, _factory, _performer, _stateChanger) #pragma warning restore 618 - : new BackgroundJobServer(_options, _storage, _additionalProcesses); + : new BackgroundJobServer(_options, _storage, _clock, _additionalProcesses); return Task.CompletedTask; } diff --git a/src/Hangfire.AspNetCore/Dashboard/AspNetCoreDashboardContext.cs b/src/Hangfire.AspNetCore/Dashboard/AspNetCoreDashboardContext.cs index c0b992bba..f4627179c 100644 --- a/src/Hangfire.AspNetCore/Dashboard/AspNetCoreDashboardContext.cs +++ b/src/Hangfire.AspNetCore/Dashboard/AspNetCoreDashboardContext.cs @@ -1,17 +1,17 @@ // This file is part of Hangfire. // Copyright © 2016 Sergey Odinokov. -// +// // Hangfire is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation, either version 3 +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation, either version 3 // of the License, or any later version. -// +// // Hangfire is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public +// +// You should have received a copy of the GNU Lesser General Public // License along with Hangfire. If not, see . using System; @@ -26,9 +26,10 @@ public sealed class AspNetCoreDashboardContext : DashboardContext { public AspNetCoreDashboardContext( [NotNull] JobStorage storage, + [NotNull] IClock clock, [NotNull] DashboardOptions options, - [NotNull] HttpContext httpContext) - : base(storage, options) + [NotNull] HttpContext httpContext) + : base(storage, clock, options) { if (httpContext == null) throw new ArgumentNullException(nameof(httpContext)); diff --git a/src/Hangfire.AspNetCore/Dashboard/AspNetCoreDashboardMiddleware.cs b/src/Hangfire.AspNetCore/Dashboard/AspNetCoreDashboardMiddleware.cs index 77a09e0c0..969eaa040 100644 --- a/src/Hangfire.AspNetCore/Dashboard/AspNetCoreDashboardMiddleware.cs +++ b/src/Hangfire.AspNetCore/Dashboard/AspNetCoreDashboardMiddleware.cs @@ -1,17 +1,17 @@ // This file is part of Hangfire. -// Copyright © 2016 Sergey Odinokov. -// +// Copyright � 2016 Sergey Odinokov. +// // Hangfire is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation, either version 3 +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation, either version 3 // of the License, or any later version. -// +// // Hangfire is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public +// +// You should have received a copy of the GNU Lesser General Public // License along with Hangfire. If not, see . using System; @@ -28,31 +28,35 @@ public class AspNetCoreDashboardMiddleware { private readonly RequestDelegate _next; private readonly JobStorage _storage; + private readonly IClock _clock; private readonly DashboardOptions _options; private readonly RouteCollection _routes; public AspNetCoreDashboardMiddleware( [NotNull] RequestDelegate next, [NotNull] JobStorage storage, + [NotNull] IClock clock, [NotNull] DashboardOptions options, [NotNull] RouteCollection routes) { if (next == null) throw new ArgumentNullException(nameof(next)); if (storage == null) throw new ArgumentNullException(nameof(storage)); + if (clock == null) throw new ArgumentNullException(nameof(clock)); if (options == null) throw new ArgumentNullException(nameof(options)); if (routes == null) throw new ArgumentNullException(nameof(routes)); _next = next; _storage = storage; + _clock = clock; _options = options; _routes = routes; } public async Task Invoke(HttpContext httpContext) { - var context = new AspNetCoreDashboardContext(_storage, _options, httpContext); + var context = new AspNetCoreDashboardContext(_storage, _clock, _options, httpContext); var findResult = _routes.FindDispatcher(httpContext.Request.Path.Value); - + if (findResult == null) { await _next.Invoke(httpContext); @@ -96,4 +100,4 @@ public async Task Invoke(HttpContext httpContext) await findResult.Item1.Dispatch(context); } } -} \ No newline at end of file +} diff --git a/src/Hangfire.AspNetCore/HangfireApplicationBuilderExtensions.cs b/src/Hangfire.AspNetCore/HangfireApplicationBuilderExtensions.cs index ddc90d818..3b48add53 100644 --- a/src/Hangfire.AspNetCore/HangfireApplicationBuilderExtensions.cs +++ b/src/Hangfire.AspNetCore/HangfireApplicationBuilderExtensions.cs @@ -1,17 +1,17 @@ // This file is part of Hangfire. // Copyright © 2016 Sergey Odinokov. -// +// // Hangfire is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation, either version 3 +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation, either version 3 // of the License, or any later version. -// +// // Hangfire is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public +// +// You should have received a copy of the GNU Lesser General Public // License along with Hangfire. If not, see . using System; @@ -33,7 +33,8 @@ public static IApplicationBuilder UseHangfireDashboard( [NotNull] this IApplicationBuilder app, [NotNull] string pathMatch = "/hangfire", [CanBeNull] DashboardOptions options = null, - [CanBeNull] JobStorage storage = null) + [CanBeNull] JobStorage storage = null, + [CanBeNull] IClock clock = null) { if (app == null) throw new ArgumentNullException(nameof(app)); if (pathMatch == null) throw new ArgumentNullException(nameof(pathMatch)); @@ -43,12 +44,13 @@ public static IApplicationBuilder UseHangfireDashboard( var services = app.ApplicationServices; storage = storage ?? services.GetRequiredService(); + clock = clock ?? services.GetRequiredService(); options = options ?? services.GetService() ?? new DashboardOptions(); options.TimeZoneResolver = options.TimeZoneResolver ?? services.GetService(); var routes = app.ApplicationServices.GetRequiredService(); - app.Map(new PathString(pathMatch), x => x.UseMiddleware(storage, options, routes)); + app.Map(new PathString(pathMatch), x => x.UseMiddleware(storage, clock, options, routes)); return app; } @@ -57,16 +59,18 @@ public static IApplicationBuilder UseHangfireServer( [NotNull] this IApplicationBuilder app, [CanBeNull] BackgroundJobServerOptions options = null, [CanBeNull] IEnumerable additionalProcesses = null, - [CanBeNull] JobStorage storage = null) + [CanBeNull] JobStorage storage = null, + [CanBeNull] IClock clock = null) { if (app == null) throw new ArgumentNullException(nameof(app)); - + HangfireServiceCollectionExtensions.ThrowIfNotConfigured(app.ApplicationServices); var services = app.ApplicationServices; var lifetime = services.GetRequiredService(); storage = storage ?? services.GetRequiredService(); + clock = clock ?? services.GetRequiredService(); options = options ?? services.GetService() ?? new BackgroundJobServerOptions(); additionalProcesses = additionalProcesses ?? services.GetServices(); @@ -76,9 +80,9 @@ public static IApplicationBuilder UseHangfireServer( var server = HangfireServiceCollectionExtensions.GetInternalServices(services, out var factory, out var stateChanger, out var performer) #pragma warning disable 618 - ? new BackgroundJobServer(options, storage, additionalProcesses, null, null, factory, performer, stateChanger) + ? new BackgroundJobServer(options, storage, clock, additionalProcesses, null, null, factory, performer, stateChanger) #pragma warning restore 618 - : new BackgroundJobServer(options, storage, additionalProcesses); + : new BackgroundJobServer(options, storage, clock, additionalProcesses); lifetime.ApplicationStopping.Register(() => server.SendStop()); lifetime.ApplicationStopped.Register(() => server.Dispose()); diff --git a/src/Hangfire.AspNetCore/HangfireServiceCollectionExtensions.cs b/src/Hangfire.AspNetCore/HangfireServiceCollectionExtensions.cs index c3ab881f9..bddf5b2b1 100644 --- a/src/Hangfire.AspNetCore/HangfireServiceCollectionExtensions.cs +++ b/src/Hangfire.AspNetCore/HangfireServiceCollectionExtensions.cs @@ -1,17 +1,17 @@ // This file is part of Hangfire. // Copyright © 2016 Sergey Odinokov. -// +// // Hangfire is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation, either version 3 +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation, either version 3 // of the License, or any later version. -// +// // Hangfire is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public +// +// You should have received a copy of the GNU Lesser General Public // License along with Hangfire. If not, see . using System; @@ -27,6 +27,7 @@ using Microsoft.Extensions.Logging; #if NETSTANDARD2_0 using Microsoft.Extensions.Hosting; + #endif namespace Hangfire @@ -46,10 +47,11 @@ public static IServiceCollection AddHangfire( { if (services == null) throw new ArgumentNullException(nameof(services)); if (configuration == null) throw new ArgumentNullException(nameof(configuration)); - + // ===== Configurable services ===== services.TryAddSingletonChecked(_ => JobStorage.Current); + services.TryAddSingletonChecked(_ => SystemClock.Current); services.TryAddSingletonChecked(_ => JobActivator.Current); services.TryAddSingletonChecked(_ => DashboardRoutes.Routes); services.TryAddSingletonChecked(_ => JobFilterProviders.Providers); @@ -64,18 +66,19 @@ public static IServiceCollection AddHangfire( // ===== Client services ===== - // NOTE: these, on the other hand, need to be double-checked to be sure configuration block was executed, + // NOTE: these, on the other hand, need to be double-checked to be sure configuration block was executed, // in case of a client-only scenario with all configurables above replaced with custom implementations. services.TryAddSingletonChecked(x => { if (GetInternalServices(x, out var factory, out var stateChanger, out _)) { - return new BackgroundJobClient(x.GetRequiredService(), factory, stateChanger); + return new BackgroundJobClient(x.GetRequiredService(), x.GetRequiredService(), factory, stateChanger); } return new BackgroundJobClient( x.GetRequiredService(), + x.GetRequiredService(), x.GetRequiredService()); }); @@ -85,23 +88,25 @@ public static IServiceCollection AddHangfire( { return new RecurringJobManager( x.GetRequiredService(), + x.GetRequiredService(), factory, x.GetRequiredService()); } return new RecurringJobManager( x.GetRequiredService(), + x.GetRequiredService(), x.GetRequiredService(), x.GetRequiredService()); }); - // IGlobalConfiguration serves as a marker indicating that Hangfire's services + // IGlobalConfiguration serves as a marker indicating that Hangfire's services // were added to the service container (checked by IApplicationBuilder extensions). - // - // Being a singleton, it also guarantees that the configuration callback will be + // + // Being a singleton, it also guarantees that the configuration callback will be // executed just once upon initialization, so there's no need to double-check that. - // + // // It should never be replaced by another implementation !!! // AddSingleton() will throw an exception if it was already registered @@ -127,10 +132,10 @@ public static IServiceCollection AddHangfire( // do configuration inside callback configuration(serviceProvider, configurationInstance); - + return configurationInstance; }); - + return services; } @@ -145,6 +150,7 @@ public static IServiceCollection AddHangfireServer([NotNull] this IServiceCollec var options = provider.GetService() ?? new BackgroundJobServerOptions(); var storage = provider.GetService() ?? JobStorage.Current; + var clock = provider.GetService() ?? SystemClock.Current; var additionalProcesses = provider.GetServices(); options.Activator = options.Activator ?? provider.GetService(); @@ -156,7 +162,7 @@ public static IServiceCollection AddHangfireServer([NotNull] this IServiceCollec #pragma warning disable 618 return new BackgroundJobServerHostedService( #pragma warning restore 618 - storage, options, additionalProcesses, factory, performer, stateChanger); + storage, clock, options, additionalProcesses, factory, performer, stateChanger); }); return services; @@ -186,7 +192,7 @@ internal static bool GetInternalServices( } private static void TryAddSingletonChecked( - [NotNull] this IServiceCollection serviceCollection, + [NotNull] this IServiceCollection serviceCollection, [NotNull] Func implementationFactory) where T : class { @@ -211,4 +217,4 @@ internal static void ThrowIfNotConfigured(IServiceProvider serviceProvider) } } } -} +} \ No newline at end of file diff --git a/src/Hangfire.Core/AppBuilderExtensions.cs b/src/Hangfire.Core/AppBuilderExtensions.cs index d1d388d88..9a0401542 100644 --- a/src/Hangfire.Core/AppBuilderExtensions.cs +++ b/src/Hangfire.Core/AppBuilderExtensions.cs @@ -1,17 +1,17 @@ // This file is part of Hangfire. // Copyright © 2013-2014 Sergey Odinokov. -// +// // Hangfire is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation, either version 3 +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation, either version 3 // of the License, or any later version. -// +// // Hangfire is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public +// +// You should have received a copy of the GNU Lesser General Public // License along with Hangfire. If not, see . using System; @@ -36,28 +36,28 @@ namespace Hangfire Func< Func, Task>, Func, Task> - >>>; + >>>; /// /// Provides extension methods for the IAppBuilder interface - /// defined in the Owin + /// defined in the Owin /// NuGet package to simplify the integration with OWIN applications. /// - /// + /// /// - /// + /// /// This class simplifies Hangfire configuration in OWIN applications, /// please read /// Getting Started with OWIN and Katana if you aren't familiar with OWIN /// and/or don't know what is the Startup class. /// - /// - /// The methods of this class should be called from OWIN's Startup + /// + /// The methods of this class should be called from OWIN's Startup /// class. - /// + /// ///

UseHangfireDashboard

/// Dashboard UI contains pages that allow you to monitor almost every - /// aspect of background processing. It is exposed as an OWIN middleware that + /// aspect of background processing. It is exposed as an OWIN middleware that /// intercepts requests to the given path. /// OWIN implementation of Dashboard UI allows to use it outside of web /// applications, including console applications and Windows Services. @@ -67,65 +67,65 @@ namespace Hangfire /// production, make sure you still have access to the Dashboard UI by using the /// /// Hangfire.Dashboard.Authorization package. - /// + /// ///

UseHangfireServer

- /// In addition to creation of a new instance of the - /// class, these methods also register the call to its - /// method on application shutdown. This is done via registering a callback on the corresponding - /// from OWIN environment ("host.OnAppDisposing" or + /// In addition to creation of a new instance of the + /// class, these methods also register the call to its + /// method on application shutdown. This is done via registering a callback on the corresponding + /// from OWIN environment ("host.OnAppDisposing" or /// "server.OnDispose" keys). /// This enables graceful shutdown feature for background jobs and background processes /// without any additional configuration. /// Please see for more details regarding /// background processing. ///
- /// + /// /// - ///

Basic Configuration

+ ///

Basic Configuration

/// Basic setup in an OWIN application looks like the following example. Please note /// that job storage should be configured before using the methods of this class. - /// + /// /// - /// + /// ///

Adding Dashboard Only

/// If you want to install dashboard without starting a background job server, for example, /// to process background jobs outside of your web application, call only the /// . - /// + /// /// - /// + /// ///

Change Dashboard Path

/// By default, you can access Dashboard UI by hitting the http(s)://<app>/hangfire /// URL, however you can change it as in the following example. - /// + /// /// - /// + /// ///

Configuring Authorization

/// The following example demonstrates how to change default local-requests-only /// authorization for Dashboard UI. - /// + /// /// - /// + /// ///

Changing Application Path

/// Have you seen the Back to site button in the Dashboard? By default it leads /// you to the root of your site, but you can configure the behavior. - /// + /// /// - /// + /// ///

Multiple Dashboards

/// The following example demonstrates adding multiple Dashboard UI endpoints. This may /// be useful when you are using multiple shards for your background processing needs. - /// + /// /// - /// + /// ///
- /// + /// /// /// /// /// Hangfire.Dashboard.Authorization Package /// - /// + /// /// [EditorBrowsable(EditorBrowsableState.Never)] public static class AppBuilderExtensions @@ -141,12 +141,12 @@ private static readonly ConcurrentBag Servers /// registers its disposal on application shutdown. /// /// OWIN application builder. - /// + /// /// is null. /// /// OWIN environment does not contain the application shutdown cancellation token. /// - /// + /// /// /// Please see for details and examples. /// @@ -156,28 +156,28 @@ public static IAppBuilder UseHangfireServer([NotNull] this IAppBuilder builder) } /// - /// Creates a new instance of the class - /// with the given collection of additional background processes and + /// Creates a new instance of the class + /// with the given collection of additional background processes and /// storage, and registers its disposal /// on application shutdown. /// /// OWIN application builder. /// Collection of additional background processes. - /// + /// /// is null. /// is null. /// /// OWIN environment does not contain the application shutdown cancellation token. /// - /// + /// /// /// Please see for details and examples. /// public static IAppBuilder UseHangfireServer( - [NotNull] this IAppBuilder builder, + [NotNull] this IAppBuilder builder, [NotNull] params IBackgroundProcess[] additionalProcesses) { - return builder.UseHangfireServer(JobStorage.Current, new BackgroundJobServerOptions(), additionalProcesses); + return builder.UseHangfireServer(JobStorage.Current, SystemClock.Current, new BackgroundJobServerOptions(), additionalProcesses); } /// @@ -187,13 +187,13 @@ public static IAppBuilder UseHangfireServer( /// /// OWIN application builder. /// Options for background job server. - /// + /// /// is null. /// is null. /// /// OWIN environment does not contain the application shutdown cancellation token. /// - /// + /// /// /// Please see for details and examples. /// @@ -201,7 +201,7 @@ public static IAppBuilder UseHangfireServer( [NotNull] this IAppBuilder builder, [NotNull] BackgroundJobServerOptions options) { - return builder.UseHangfireServer(options, JobStorage.Current); + return builder.UseHangfireServer(options, JobStorage.Current, SystemClock.Current); } /// @@ -213,14 +213,14 @@ public static IAppBuilder UseHangfireServer( /// OWIN application builder. /// Options for background job server. /// Collection of additional background processes. - /// + /// /// is null. /// is null. /// is null. /// /// OWIN environment does not contain the application shutdown cancellation token. /// - /// + /// /// /// Please see for details and examples. /// @@ -229,7 +229,7 @@ public static IAppBuilder UseHangfireServer( [NotNull] BackgroundJobServerOptions options, [NotNull] params IBackgroundProcess[] additionalProcesses) { - return builder.UseHangfireServer(JobStorage.Current, options, additionalProcesses); + return builder.UseHangfireServer(JobStorage.Current, SystemClock.Current, options, additionalProcesses); } /// @@ -240,35 +240,37 @@ public static IAppBuilder UseHangfireServer( /// OWIN application builder. /// Options for background job server. /// Storage to use by background job server. - /// + /// Clock /// is null. /// is null. /// is null. /// /// OWIN environment does not contain the application shutdown cancellation token. /// - /// + /// /// /// Please see for details and examples. /// public static IAppBuilder UseHangfireServer( [NotNull] this IAppBuilder builder, [NotNull] BackgroundJobServerOptions options, - [NotNull] JobStorage storage) + [NotNull] JobStorage storage, + [NotNull] IClock clock) { - return builder.UseHangfireServer(storage, options); + return builder.UseHangfireServer(storage, clock, options); } /// /// Starts a new instance of the class with /// the given arguments, and registers its disposal on application shutdown. /// - /// + /// /// OWIN application builder. /// Storage to use by background job server. + /// Clock /// Options for background job server. /// Collection of additional background processes. - /// + /// /// is null. /// is null. /// is null. @@ -276,22 +278,24 @@ public static IAppBuilder UseHangfireServer( /// /// OWIN environment does not contain the application shutdown cancellation token. /// - /// + /// /// /// Please see for details and examples. /// public static IAppBuilder UseHangfireServer( [NotNull] this IAppBuilder builder, [NotNull] JobStorage storage, - [NotNull] BackgroundJobServerOptions options, + [NotNull] IClock clock, + [NotNull] BackgroundJobServerOptions options, [NotNull] params IBackgroundProcess[] additionalProcesses) { if (builder == null) throw new ArgumentNullException(nameof(builder)); if (storage == null) throw new ArgumentNullException(nameof(storage)); + if (clock == null) throw new ArgumentNullException(nameof(clock)); if (options == null) throw new ArgumentNullException(nameof(options)); if (additionalProcesses == null) throw new ArgumentNullException(nameof(additionalProcesses)); - var server = new BackgroundJobServer(options, storage, additionalProcesses); + var server = new BackgroundJobServer(options, storage, clock, additionalProcesses); Servers.Add(server); var context = new OwinContext(builder.Properties); @@ -315,13 +319,13 @@ public static IAppBuilder UseHangfireServer( } /// - /// Adds Dashboard UI middleware to the OWIN request processing pipeline under + /// Adds Dashboard UI middleware to the OWIN request processing pipeline under /// the /hangfire path, for the storage. /// /// OWIN application builder. - /// + /// /// is null. - /// + /// /// /// Please see for details and examples. /// @@ -336,10 +340,10 @@ public static IAppBuilder UseHangfireDashboard([NotNull] this IAppBuilder builde /// /// OWIN application builder. /// Path prefix for middleware to use, e.g. "/hangfire". - /// + /// /// is null. /// is null. - /// + /// /// /// Please see for details and examples. /// @@ -358,11 +362,11 @@ public static IAppBuilder UseHangfireDashboard( /// OWIN application builder. /// Path prefix for middleware to use, e.g. "/hangfire". /// Options for Dashboard UI. - /// + /// /// is null. /// is null. /// is null. - /// + /// /// /// Please see for details and examples. /// @@ -371,7 +375,7 @@ public static IAppBuilder UseHangfireDashboard( [NotNull] string pathMatch, [NotNull] DashboardOptions options) { - return builder.UseHangfireDashboard(pathMatch, options, JobStorage.Current); + return builder.UseHangfireDashboard(pathMatch, options, JobStorage.Current, SystemClock.Current); } /// @@ -382,12 +386,12 @@ public static IAppBuilder UseHangfireDashboard( /// Path prefix for middleware to use, e.g. "/hangfire". /// Options for Dashboard UI. /// Job storage to use by Dashboard IO. - /// + /// Clock /// is null. /// is null. /// is null. /// is null. - /// + /// /// /// Please see for details and examples. /// @@ -395,9 +399,10 @@ public static IAppBuilder UseHangfireDashboard( [NotNull] this IAppBuilder builder, [NotNull] string pathMatch, [NotNull] DashboardOptions options, - [NotNull] JobStorage storage) + [NotNull] JobStorage storage, + [NotNull] IClock clock) { - return builder.UseHangfireDashboard(pathMatch, options, storage, null); + return builder.UseHangfireDashboard(pathMatch, options, storage, clock, null); } /// @@ -408,22 +413,24 @@ public static IAppBuilder UseHangfireDashboard( /// Path prefix for middleware to use, e.g. "/hangfire". /// Options for Dashboard UI. /// Job storage to use by Dashboard IO. + /// Clock /// Antiforgery service. - /// + /// /// is null. /// is null. /// is null. /// is null. - /// + /// /// /// Please see for details and examples. /// public static IAppBuilder UseHangfireDashboard( - [NotNull] this IAppBuilder builder, - [NotNull] string pathMatch, - [NotNull] DashboardOptions options, - [NotNull] JobStorage storage, - [CanBeNull] IOwinDashboardAntiforgery antiforgery) + [NotNull] this IAppBuilder builder, + [NotNull] string pathMatch, + [NotNull] DashboardOptions options, + [NotNull] JobStorage storage, + [NotNull] IClock clock, + [CanBeNull] IOwinDashboardAntiforgery antiforgery) { if (builder == null) throw new ArgumentNullException(nameof(builder)); if (pathMatch == null) throw new ArgumentNullException(nameof(pathMatch)); @@ -434,7 +441,7 @@ public static IAppBuilder UseHangfireDashboard( builder.Map(pathMatch, subApp => subApp .UseOwin() - .UseHangfireDashboard(options, storage, DashboardRoutes.Routes, antiforgery)); + .UseHangfireDashboard(options, storage, clock, DashboardRoutes.Routes, antiforgery)); return builder; } diff --git a/src/Hangfire.Core/BackgroundJobClient.cs b/src/Hangfire.Core/BackgroundJobClient.cs index a3a1265d6..aa23a715f 100644 --- a/src/Hangfire.Core/BackgroundJobClient.cs +++ b/src/Hangfire.Core/BackgroundJobClient.cs @@ -1,17 +1,17 @@ // This file is part of Hangfire. // Copyright © 2013-2014 Sergey Odinokov. -// +// // Hangfire is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation, either version 3 +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation, either version 3 // of the License, or any later version. -// +// // Hangfire is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public +// +// You should have received a copy of the GNU Lesser General Public // License along with Hangfire. If not, see . using System; @@ -23,30 +23,31 @@ namespace Hangfire { /// - /// Provides methods for creating all the types of background jobs and - /// changing their states. Represents a default implementation of the + /// Provides methods for creating all the types of background jobs and + /// changing their states. Represents a default implementation of the /// interface. /// - /// + /// /// - /// This class uses the interface - /// for creating background jobs and the - /// interface for changing their states. Please see documentation for those + /// This class uses the interface + /// for creating background jobs and the + /// interface for changing their states. Please see documentation for those /// types and their implementations to learn the details. - /// + /// /// /// Despite the fact that instance methods of this class are thread-safe, /// most implementations of the interface are neither - /// thread-safe, nor immutable. Please create a new instance of a state - /// class for each operation to avoid race conditions and unexpected side + /// thread-safe, nor immutable. Please create a new instance of a state + /// class for each operation to avoid race conditions and unexpected side /// effects. /// /// - /// + /// /// public class BackgroundJobClient : IBackgroundJobClient { private readonly JobStorage _storage; + private readonly IClock _clock; private readonly IBackgroundJobFactory _factory; private readonly IBackgroundJobStateChanger _stateChanger; @@ -54,26 +55,26 @@ public class BackgroundJobClient : IBackgroundJobClient /// Initializes a new instance of the /// class with the storage from a global configuration. /// - /// + /// /// /// Please see the class for the /// details regarding the global configuration. /// public BackgroundJobClient() - : this(JobStorage.Current) + : this(JobStorage.Current, SystemClock.Current) { } - + /// /// Initializes a new instance of the /// class with the specified storage. /// - /// + /// /// Job storage to use for background jobs. - /// + /// Clock /// is null. - public BackgroundJobClient([NotNull] JobStorage storage) - : this(storage, JobFilterProviders.Providers) + public BackgroundJobClient([NotNull] JobStorage storage, [NotNull] IClock clock) + : this(storage, clock, JobFilterProviders.Providers) { } @@ -82,36 +83,41 @@ public BackgroundJobClient([NotNull] JobStorage storage) /// with the specified storage and filter provider. /// /// Job storage to use for background jobs. + /// Clock /// Filter provider responsible to locate job filters. /// is null. /// is null. - public BackgroundJobClient([NotNull] JobStorage storage, [NotNull] IJobFilterProvider filterProvider) - : this(storage, new BackgroundJobFactory(filterProvider), new BackgroundJobStateChanger(filterProvider)) + public BackgroundJobClient([NotNull] JobStorage storage, [NotNull] IClock clock, [NotNull] IJobFilterProvider filterProvider) + : this(storage, clock, new BackgroundJobFactory(filterProvider), new BackgroundJobStateChanger(filterProvider)) { } - + /// /// Initializes a new instance of the class /// with the specified storage, background job factory and state changer. /// - /// + /// /// Job storage to use for background jobs. + /// Clock /// Factory to create background jobs. /// State changer to change states of background jobs. - /// + /// /// is null. /// is null. /// is null. public BackgroundJobClient( [NotNull] JobStorage storage, + [NotNull] IClock clock, [NotNull] IBackgroundJobFactory factory, [NotNull] IBackgroundJobStateChanger stateChanger) { if (storage == null) throw new ArgumentNullException(nameof(storage)); + if (clock == null) throw new ArgumentNullException(nameof(clock)); if (factory == null) throw new ArgumentNullException(nameof(factory)); if (stateChanger == null) throw new ArgumentNullException(nameof(stateChanger)); - + _storage = storage; + _clock = clock; _stateChanger = stateChanger; _factory = factory; } @@ -126,7 +132,7 @@ public string Create(Job job, IState state) { using (var connection = _storage.GetConnection()) { - var context = new CreateContext(_storage, connection, job, state); + var context = new CreateContext(_storage, _clock, connection, job, state); var backroundJob = _factory.Create(context); return backroundJob?.Id; @@ -150,6 +156,7 @@ public bool ChangeState(string jobId, IState state, string expectedState) { var appliedState = _stateChanger.ChangeState(new StateChangeContext( _storage, + _clock, connection, jobId, state, diff --git a/src/Hangfire.Core/BackgroundJobServer.cs b/src/Hangfire.Core/BackgroundJobServer.cs index 11fe711bf..9fde63c20 100644 --- a/src/Hangfire.Core/BackgroundJobServer.cs +++ b/src/Hangfire.Core/BackgroundJobServer.cs @@ -1,17 +1,17 @@ // This file is part of Hangfire. // Copyright © 2013-2014 Sergey Odinokov. -// +// // Hangfire is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation, either version 3 +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation, either version 3 // of the License, or any later version. -// +// // Hangfire is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public +// +// You should have received a copy of the GNU Lesser General Public // License along with Hangfire. If not, see . using System; @@ -50,8 +50,8 @@ public BackgroundJobServer() /// with default options and the given storage. /// /// The storage - public BackgroundJobServer([NotNull] JobStorage storage) - : this(new BackgroundJobServerOptions(), storage) + public BackgroundJobServer([NotNull] JobStorage storage, [NotNull] IClock clock) + : this(new BackgroundJobServerOptions(), storage, clock) { } @@ -61,7 +61,7 @@ public BackgroundJobServer([NotNull] JobStorage storage) /// /// Server options public BackgroundJobServer([NotNull] BackgroundJobServerOptions options) - : this(options, JobStorage.Current) + : this(options, JobStorage.Current, SystemClock.Current) { } @@ -71,17 +71,18 @@ public BackgroundJobServer([NotNull] BackgroundJobServerOptions options) /// /// Server options /// The storage - public BackgroundJobServer([NotNull] BackgroundJobServerOptions options, [NotNull] JobStorage storage) - : this(options, storage, Enumerable.Empty()) + public BackgroundJobServer([NotNull] BackgroundJobServerOptions options, [NotNull] JobStorage storage, [NotNull] IClock clock) + : this(options, storage, clock, Enumerable.Empty()) { } public BackgroundJobServer( [NotNull] BackgroundJobServerOptions options, [NotNull] JobStorage storage, + [NotNull] IClock clock, [NotNull] IEnumerable additionalProcesses) #pragma warning disable 618 - : this(options, storage, additionalProcesses, null, null, null, null, null) + : this(options, storage, clock, additionalProcesses, null, null, null, null, null) #pragma warning restore 618 { } @@ -91,6 +92,7 @@ public BackgroundJobServer( public BackgroundJobServer( [NotNull] BackgroundJobServerOptions options, [NotNull] JobStorage storage, + [NotNull] IClock clock, [NotNull] IEnumerable additionalProcesses, [CanBeNull] IJobFilterProvider filterProvider, [CanBeNull] JobActivator activator, @@ -99,6 +101,7 @@ public BackgroundJobServer( [CanBeNull] IBackgroundJobStateChanger stateChanger) { if (storage == null) throw new ArgumentNullException(nameof(storage)); + if (clock == null) throw new ArgumentNullException(nameof(clock)); if (options == null) throw new ArgumentNullException(nameof(options)); if (additionalProcesses == null) throw new ArgumentNullException(nameof(additionalProcesses)); @@ -119,15 +122,16 @@ public BackgroundJobServer( storage.WriteOptionsToLog(_logger); _logger.Info("Using the following options for Hangfire Server:\r\n" + - $" Worker count: {options.WorkerCount}\r\n" + - $" Listening queues: {String.Join(", ", options.Queues.Select(x => "'" + x + "'"))}\r\n" + - $" Shutdown timeout: {options.ShutdownTimeout}\r\n" + - $" Schedule polling interval: {options.SchedulePollingInterval}"); - + $" Worker count: {options.WorkerCount}\r\n" + + $" Listening queues: {String.Join(", ", options.Queues.Select(x => "'" + x + "'"))}\r\n" + + $" Shutdown timeout: {options.ShutdownTimeout}\r\n" + + $" Schedule polling interval: {options.SchedulePollingInterval}"); + _processingServer = new BackgroundProcessingServer( - storage, - processes, - properties, + storage, + clock, + processes, + properties, GetProcessingServerOptions()); } @@ -169,8 +173,7 @@ public Task WaitForShutdownAsync(CancellationToken cancellationToken) return _processingServer.WaitForShutdownAsync(cancellationToken); } - private IEnumerable GetRequiredProcesses( - [CanBeNull] IJobFilterProvider filterProvider, + private IEnumerable GetRequiredProcesses([CanBeNull] IJobFilterProvider filterProvider, [CanBeNull] JobActivator activator, [CanBeNull] IBackgroundJobFactory factory, [CanBeNull] IBackgroundJobPerformer performer, diff --git a/src/Hangfire.Core/Client/CoreBackgroundJobFactory.cs b/src/Hangfire.Core/Client/CoreBackgroundJobFactory.cs index 6d0ba31b6..3e27e6e70 100644 --- a/src/Hangfire.Core/Client/CoreBackgroundJobFactory.cs +++ b/src/Hangfire.Core/Client/CoreBackgroundJobFactory.cs @@ -1,17 +1,17 @@ // This file is part of Hangfire. // Copyright © 2013-2014 Sergey Odinokov. -// +// // Hangfire is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation, either version 3 +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation, either version 3 // of the License, or any later version. -// +// // Hangfire is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public +// +// You should have received a copy of the GNU Lesser General Public // License along with Hangfire. If not, see . using System; @@ -40,7 +40,7 @@ public BackgroundJob Create(CreateContext context) x => x.Key, x => SerializationHelper.Serialize(x.Value, SerializationOption.User)); - var createdAt = DateTime.UtcNow; + var createdAt = context.Clock.UtcNow; var jobId = context.Connection.CreateExpiredJob( context.Job, parameters, @@ -55,6 +55,7 @@ public BackgroundJob Create(CreateContext context) { var applyContext = new ApplyStateContext( context.Storage, + context.Clock, context.Connection, transaction, backgroundJob, @@ -71,4 +72,4 @@ public BackgroundJob Create(CreateContext context) return backgroundJob; } } -} \ No newline at end of file +} diff --git a/src/Hangfire.Core/Client/CreateContext.cs b/src/Hangfire.Core/Client/CreateContext.cs index bf29cbd56..e2e85f083 100644 --- a/src/Hangfire.Core/Client/CreateContext.cs +++ b/src/Hangfire.Core/Client/CreateContext.cs @@ -1,17 +1,17 @@ // This file is part of Hangfire. // Copyright © 2013-2014 Sergey Odinokov. -// +// // Hangfire is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation, either version 3 +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation, either version 3 // of the License, or any later version. -// +// // Hangfire is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public +// +// You should have received a copy of the GNU Lesser General Public // License along with Hangfire. If not, see . using System; @@ -30,7 +30,7 @@ namespace Hangfire.Client public class CreateContext { public CreateContext([NotNull] CreateContext context) - : this(context.Storage, context.Connection, context.Job, context.InitialState, context.Profiler) + : this(context.Storage, context.Clock, context.Connection, context.Job, context.InitialState, context.Profiler) { Items = context.Items; Parameters = context.Parameters; @@ -38,25 +38,29 @@ public CreateContext([NotNull] CreateContext context) public CreateContext( [NotNull] JobStorage storage, + [NotNull] IClock clock, [NotNull] IStorageConnection connection, [NotNull] Job job, [CanBeNull] IState initialState) - : this(storage, connection, job, initialState, EmptyProfiler.Instance) + : this(storage, clock, connection, job, initialState, EmptyProfiler.Instance) { } internal CreateContext( - [NotNull] JobStorage storage, - [NotNull] IStorageConnection connection, - [NotNull] Job job, + [NotNull] JobStorage storage, + [NotNull] IClock clock, + [NotNull] IStorageConnection connection, + [NotNull] Job job, [CanBeNull] IState initialState, [NotNull] IProfiler profiler) { if (storage == null) throw new ArgumentNullException(nameof(storage)); + if (clock == null) throw new ArgumentNullException(nameof(clock)); if (connection == null) throw new ArgumentNullException(nameof(connection)); if (job == null) throw new ArgumentNullException(nameof(job)); Storage = storage; + Clock = clock; Connection = connection; Job = job; InitialState = initialState; @@ -66,11 +70,11 @@ internal CreateContext( Parameters = new Dictionary(); } - [NotNull] - public JobStorage Storage { get; } + [NotNull] public JobStorage Storage { get; } - [NotNull] - public IStorageConnection Connection { get; } + [NotNull] public IClock Clock { get; } + + [NotNull] public IStorageConnection Connection { get; } /// /// Gets an instance of the key-value storage. You can use it @@ -80,22 +84,19 @@ internal CreateContext( [NotNull] public IDictionary Items { get; } - [NotNull] - public virtual IDictionary Parameters { get; } - - [NotNull] - public Job Job { get; } + [NotNull] public virtual IDictionary Parameters { get; } + + [NotNull] public Job Job { get; } /// /// Gets the initial state of the creating job. Note, that - /// the final state of the created job could be changed after + /// the final state of the created job could be changed after /// the registered instances of the /// class are doing their job. /// [CanBeNull] public IState InitialState { get; } - [NotNull] - internal IProfiler Profiler { get; } + [NotNull] internal IProfiler Profiler { get; } } -} \ No newline at end of file +} diff --git a/src/Hangfire.Core/ContinuationsSupportAttribute.cs b/src/Hangfire.Core/ContinuationsSupportAttribute.cs index 73d255420..15cf41bb4 100644 --- a/src/Hangfire.Core/ContinuationsSupportAttribute.cs +++ b/src/Hangfire.Core/ContinuationsSupportAttribute.cs @@ -1,17 +1,17 @@ // This file is part of Hangfire. // Copyright © 2013-2014 Sergey Odinokov. -// +// // Hangfire is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation, either version 3 +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation, either version 3 // of the License, or any later version. -// +// // Hangfire is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public +// +// You should have received a copy of the GNU Lesser General Public // License along with Hangfire. If not, see . using System; @@ -48,7 +48,7 @@ public ContinuationsSupportAttribute(HashSet knownFinalStates) } public ContinuationsSupportAttribute( - [NotNull] HashSet knownFinalStates, + [NotNull] HashSet knownFinalStates, [NotNull] IBackgroundJobStateChanger stateChanger) { if (knownFinalStates == null) throw new ArgumentNullException(nameof(knownFinalStates)); @@ -91,8 +91,8 @@ private void AddContinuation(ElectStateContext context, AwaitingState awaitingSt var connection = context.Connection; var parentId = awaitingState.ParentId; - // We store continuations as a json array in a job parameter. Since there - // is no way to add a continuation in an atomic way, we are placing a + // We store continuations as a json array in a job parameter. Since there + // is no way to add a continuation in an atomic way, we are placing a // distributed lock on parent job to prevent race conditions, when // multiple threads add continuation to the same parent job. using (connection.AcquireDistributedJobLock(parentId, AddJobLockTimeout)) @@ -143,7 +143,7 @@ private void ExecuteContinuationsIfExist(ElectStateContext context) var continuations = GetContinuations(context.Connection, context.BackgroundJob.Id); var nextStates = new Dictionary(); - // Getting continuation data for all continuations – state they are waiting + // Getting continuation data for all continuations – state they are waiting // for and their next state. foreach (var continuation in continuations) { @@ -155,7 +155,7 @@ private void ExecuteContinuationsIfExist(ElectStateContext context) continue; } - // All continuations should be in the awaiting state. If someone changed + // All continuations should be in the awaiting state. If someone changed // the state of a continuation, we should simply skip it. if (currentState.Name != AwaitingState.StateName) continue; @@ -189,11 +189,12 @@ private void ExecuteContinuationsIfExist(ElectStateContext context) nextStates.Add(continuation.JobId, nextState); } } - + foreach (var tuple in nextStates) { _stateChanger.ChangeState(new StateChangeContext( context.Storage, + context.Clock, context.Connection, tuple.Key, tuple.Value, @@ -225,7 +226,7 @@ private StateData GetContinuationState(ElectStateContext context, string continu break; } - if (DateTime.UtcNow - continuationData.CreatedAt > ContinuationInvalidTimeout) + if (context.Clock.UtcNow - continuationData.CreatedAt > ContinuationInvalidTimeout) { _logger.Warn( $"Continuation '{continuationJobId}' has been ignored: it was deemed to be aborted, because its state is still non-initialized."); diff --git a/src/Hangfire.Core/Dashboard/DashboardContext.cs b/src/Hangfire.Core/Dashboard/DashboardContext.cs index b964df95a..dd29f820e 100644 --- a/src/Hangfire.Core/Dashboard/DashboardContext.cs +++ b/src/Hangfire.Core/Dashboard/DashboardContext.cs @@ -1,17 +1,17 @@ // This file is part of Hangfire. // Copyright © 2016 Sergey Odinokov. -// +// // Hangfire is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation, either version 3 +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation, either version 3 // of the License, or any later version. -// +// // Hangfire is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public +// +// You should have received a copy of the GNU Lesser General Public // License along with Hangfire. If not, see . using System; @@ -24,21 +24,24 @@ public abstract class DashboardContext { private readonly Lazy _isReadOnlyLazy; - protected DashboardContext([NotNull] JobStorage storage, [NotNull] DashboardOptions options) + protected DashboardContext([NotNull] JobStorage storage, [NotNull] IClock clock, [NotNull] DashboardOptions options) { if (storage == null) throw new ArgumentNullException(nameof(storage)); + if (clock == null) throw new ArgumentNullException(nameof(clock)); if (options == null) throw new ArgumentNullException(nameof(options)); Storage = storage; + Clock = clock; Options = options; _isReadOnlyLazy = new Lazy(() => options.IsReadOnlyFunc(this)); } public JobStorage Storage { get; } + public IClock Clock { get; } public DashboardOptions Options { get; } public Match UriMatch { get; set; } - + public DashboardRequest Request { get; protected set; } public DashboardResponse Response { get; protected set; } @@ -49,12 +52,12 @@ protected DashboardContext([NotNull] JobStorage storage, [NotNull] DashboardOpti public virtual IBackgroundJobClient GetBackgroundJobClient() { - return new BackgroundJobClient(Storage); + return new BackgroundJobClient(Storage, Clock); } public virtual IRecurringJobManager GetRecurringJobManager() { - return new RecurringJobManager(Storage); + return new RecurringJobManager(Storage, Clock); } } -} \ No newline at end of file +} diff --git a/src/Hangfire.Core/Dashboard/Owin/MiddlewareExtensions.cs b/src/Hangfire.Core/Dashboard/Owin/MiddlewareExtensions.cs index 3497b6158..f9ee30853 100644 --- a/src/Hangfire.Core/Dashboard/Owin/MiddlewareExtensions.cs +++ b/src/Hangfire.Core/Dashboard/Owin/MiddlewareExtensions.cs @@ -1,17 +1,17 @@ // This file is part of Hangfire. // Copyright © 2013-2014 Sergey Odinokov. -// +// // Hangfire is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation, either version 3 +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation, either version 3 // of the License, or any later version. -// +// // Hangfire is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public +// +// You should have received a copy of the GNU Lesser General Public // License along with Hangfire. If not, see . using System; @@ -43,29 +43,33 @@ public static class MiddlewareExtensions { public static BuildFunc UseHangfireDashboard( [NotNull] this BuildFunc builder, - [NotNull] DashboardOptions options, - [NotNull] JobStorage storage, + [NotNull] DashboardOptions options, + [NotNull] JobStorage storage, + [NotNull] IClock clock, [NotNull] RouteCollection routes, [CanBeNull] IOwinDashboardAntiforgery antiforgery) { if (builder == null) throw new ArgumentNullException(nameof(builder)); if (options == null) throw new ArgumentNullException(nameof(options)); if (storage == null) throw new ArgumentNullException(nameof(storage)); + if (clock == null) throw new ArgumentNullException(nameof(clock)); if (routes == null) throw new ArgumentNullException(nameof(routes)); - builder(_ => UseHangfireDashboard(options, storage, routes, antiforgery)); + builder(_ => UseHangfireDashboard(options, storage, clock, routes, antiforgery)); return builder; } public static MidFunc UseHangfireDashboard( - [NotNull] DashboardOptions options, - [NotNull] JobStorage storage, + [NotNull] DashboardOptions options, + [NotNull] JobStorage storage, + [NotNull] IClock clock, [NotNull] RouteCollection routes, [CanBeNull] IOwinDashboardAntiforgery antiforgery) { if (options == null) throw new ArgumentNullException(nameof(options)); if (storage == null) throw new ArgumentNullException(nameof(storage)); + if (clock == null) throw new ArgumentNullException(nameof(clock)); if (routes == null) throw new ArgumentNullException(nameof(routes)); return @@ -73,7 +77,7 @@ public static MidFunc UseHangfireDashboard( env => { var owinContext = new OwinContext(env); - var context = new OwinDashboardContext(storage, options, env); + var context = new OwinDashboardContext(storage, clock, options, env); if (!options.IgnoreAntiforgeryToken && antiforgery != null) { @@ -127,4 +131,4 @@ private static Task Unauthorized(IOwinContext owinContext) return Task.FromResult(0); } } -} \ No newline at end of file +} diff --git a/src/Hangfire.Core/Dashboard/Owin/OwinDashboardContext.cs b/src/Hangfire.Core/Dashboard/Owin/OwinDashboardContext.cs index 569502aa7..089ceff64 100644 --- a/src/Hangfire.Core/Dashboard/Owin/OwinDashboardContext.cs +++ b/src/Hangfire.Core/Dashboard/Owin/OwinDashboardContext.cs @@ -1,17 +1,17 @@ // This file is part of Hangfire. // Copyright © 2016 Sergey Odinokov. -// +// // Hangfire is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation, either version 3 +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation, either version 3 // of the License, or any later version. -// +// // Hangfire is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public +// +// You should have received a copy of the GNU Lesser General Public // License along with Hangfire. If not, see . using System; @@ -24,9 +24,10 @@ public sealed class OwinDashboardContext : DashboardContext { public OwinDashboardContext( [NotNull] JobStorage storage, + [NotNull] IClock clock, [NotNull] DashboardOptions options, - [NotNull] IDictionary environment) - : base(storage, options) + [NotNull] IDictionary environment) + : base(storage, clock, options) { if (environment == null) throw new ArgumentNullException(nameof(environment)); diff --git a/src/Hangfire.Core/Dashboard/RouteCollectionExtensions.cs b/src/Hangfire.Core/Dashboard/RouteCollectionExtensions.cs index 30c367718..feba2eb38 100644 --- a/src/Hangfire.Core/Dashboard/RouteCollectionExtensions.cs +++ b/src/Hangfire.Core/Dashboard/RouteCollectionExtensions.cs @@ -1,17 +1,17 @@ // This file is part of Hangfire. // Copyright © 2013-2014 Sergey Odinokov. -// +// // Hangfire is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation, either version 3 +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation, either version 3 // of the License, or any later version. -// +// // Hangfire is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public +// +// You should have received a copy of the GNU Lesser General Public // License along with Hangfire. If not, see . using System; @@ -24,8 +24,8 @@ namespace Hangfire.Dashboard public static class RouteCollectionExtensions { public static void AddRazorPage( - [NotNull] this RouteCollection routes, - [NotNull] string pathTemplate, + [NotNull] this RouteCollection routes, + [NotNull] string pathTemplate, [NotNull] Func pageFunc) { if (routes == null) throw new ArgumentNullException(nameof(routes)); @@ -38,8 +38,8 @@ public static void AddRazorPage( #if FEATURE_OWIN [Obsolete("Use the AddCommand(RouteCollection, string, Func) overload instead. Will be removed in 2.0.0.")] public static void AddCommand( - [NotNull] this RouteCollection routes, - [NotNull] string pathTemplate, + [NotNull] this RouteCollection routes, + [NotNull] string pathTemplate, [NotNull] Func command) { if (routes == null) throw new ArgumentNullException(nameof(routes)); @@ -65,8 +65,8 @@ public static void AddCommand( #if FEATURE_OWIN [Obsolete("Use the AddBatchCommand(RouteCollection, string, Func) overload instead. Will be removed in 2.0.0.")] public static void AddBatchCommand( - [NotNull] this RouteCollection routes, - [NotNull] string pathTemplate, + [NotNull] this RouteCollection routes, + [NotNull] string pathTemplate, [NotNull] Action command) { if (routes == null) throw new ArgumentNullException(nameof(routes)); @@ -91,7 +91,7 @@ public static void AddBatchCommand( public static void AddClientBatchCommand( this RouteCollection routes, - string pathTemplate, + string pathTemplate, [NotNull] Action command) { if (command == null) throw new ArgumentNullException(nameof(command)); @@ -116,7 +116,7 @@ public static void AddRecurringBatchCommand( command(manager, jobId); }); } - + [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("For binary compatibility only. Use overload with Action instead.")] public static void AddRecurringBatchCommand( @@ -128,7 +128,7 @@ public static void AddRecurringBatchCommand( routes.AddBatchCommand(pathTemplate, (context, jobId) => { - var manager = new RecurringJobManager(context.Storage); + var manager = new RecurringJobManager(context.Storage, context.Clock); command(manager, jobId); }); } diff --git a/src/Hangfire.Core/IClock.cs b/src/Hangfire.Core/IClock.cs new file mode 100644 index 000000000..2ccdd3c3a --- /dev/null +++ b/src/Hangfire.Core/IClock.cs @@ -0,0 +1,36 @@ +using System; + +namespace Hangfire +{ + public interface IClock + { + DateTime UtcNow { get; } + } + + public class SystemClock : IClock + { + private static readonly Lazy Cached = new Lazy(() => new SystemClock()); + private static readonly object LockObject = new object(); + private static IClock _current; + + public static IClock Current + { + get + { + lock (LockObject) + { + return _current ?? Cached.Value; + } + } + set + { + lock (LockObject) + { + _current = value; + } + } + } + + public DateTime UtcNow => DateTime.UtcNow; + } +} diff --git a/src/Hangfire.Core/LatencyTimeoutAttribute.cs b/src/Hangfire.Core/LatencyTimeoutAttribute.cs index 7cbc446b4..ccb774f61 100644 --- a/src/Hangfire.Core/LatencyTimeoutAttribute.cs +++ b/src/Hangfire.Core/LatencyTimeoutAttribute.cs @@ -16,14 +16,14 @@ public sealed class LatencyTimeoutAttribute : JobFilterAttribute, IElectStateFil private readonly ILog _logger = LogProvider.For(); private readonly int _timeoutInSeconds; - + /// /// Initializes a new instance of the /// class with the given timeout value. /// - /// Non-negative timeout value in seconds + /// Non-negative timeout value in seconds /// that will be used to determine whether to delete a job. - /// + /// /// /// has a negative value. /// @@ -54,7 +54,7 @@ public void OnStateElection(ElectStateContext context) return; } - var elapsedTime = DateTime.UtcNow - context.BackgroundJob.CreatedAt; + var elapsedTime = context.Clock.UtcNow - context.BackgroundJob.CreatedAt; if (elapsedTime.TotalSeconds > _timeoutInSeconds) { @@ -70,4 +70,4 @@ public void OnStateElection(ElectStateContext context) } } -} \ No newline at end of file +} diff --git a/src/Hangfire.Core/Obsolete/BootstrapperConfigurationExtensions.cs b/src/Hangfire.Core/Obsolete/BootstrapperConfigurationExtensions.cs index ee282fb45..ca94b130f 100644 --- a/src/Hangfire.Core/Obsolete/BootstrapperConfigurationExtensions.cs +++ b/src/Hangfire.Core/Obsolete/BootstrapperConfigurationExtensions.cs @@ -1,17 +1,17 @@ // This file is part of Hangfire. // Copyright © 2013-2014 Sergey Odinokov. -// +// // Hangfire is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation, either version 3 +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation, either version 3 // of the License, or any later version. -// +// // Hangfire is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public +// +// You should have received a copy of the GNU Lesser General Public // License along with Hangfire. If not, see . using System; @@ -120,14 +120,16 @@ public static void UseServer( /// /// Configuration /// Job storage to use + /// Clock [Obsolete("Please use `IAppBuilder.UseHangfireServer` OWIN extension method instead. Will be removed in version 2.0.0.")] public static void UseServer( this IBootstrapperConfiguration configuration, - JobStorage storage) + JobStorage storage, + IClock clock) { configuration.UseServer(() => new BackgroundJobServer( new BackgroundJobServerOptions(), - storage)); + storage, clock)); } /// @@ -139,13 +141,15 @@ public static void UseServer( /// Configuration /// Job storage to use /// Job server options + /// Clock [Obsolete("Please use `IAppBuilder.UseHangfireServer` OWIN extension method instead. Will be removed in version 2.0.0.")] public static void UseServer( this IBootstrapperConfiguration configuration, JobStorage storage, - BackgroundJobServerOptions options) + BackgroundJobServerOptions options, + IClock clock) { - configuration.UseServer(() => new BackgroundJobServer(options, storage)); + configuration.UseServer(() => new BackgroundJobServer(options, storage, clock)); } } } diff --git a/src/Hangfire.Core/Obsolete/DashboardMiddleware.cs b/src/Hangfire.Core/Obsolete/DashboardMiddleware.cs index 33f4913c1..d38f0d8a2 100644 --- a/src/Hangfire.Core/Obsolete/DashboardMiddleware.cs +++ b/src/Hangfire.Core/Obsolete/DashboardMiddleware.cs @@ -1,17 +1,17 @@ // This file is part of Hangfire. // Copyright © 2013-2014 Sergey Odinokov. -// +// // Hangfire is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation, either version 3 +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation, either version 3 // of the License, or any later version. -// +// // Hangfire is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public +// +// You should have received a copy of the GNU Lesser General Public // License along with Hangfire. If not, see . using System; @@ -31,6 +31,7 @@ internal class DashboardMiddleware : OwinMiddleware private readonly string _appPath; private readonly int _statsPollingInterval; private readonly JobStorage _storage; + private readonly IClock _clock; private readonly RouteCollection _routes; private readonly IEnumerable _authorizationFilters; @@ -39,17 +40,20 @@ public DashboardMiddleware( string appPath, int statsPollingInterval, [NotNull] JobStorage storage, - [NotNull] RouteCollection routes, + [NotNull] IClock clock, + [NotNull] RouteCollection routes, [NotNull] IEnumerable authorizationFilters) : base(next) { if (storage == null) throw new ArgumentNullException(nameof(storage)); + if (clock == null) throw new ArgumentNullException(nameof(clock)); if (routes == null) throw new ArgumentNullException(nameof(routes)); if (authorizationFilters == null) throw new ArgumentNullException(nameof(authorizationFilters)); _appPath = appPath; _statsPollingInterval = statsPollingInterval; _storage = storage; + _clock = clock; _routes = routes; _authorizationFilters = authorizationFilters; } @@ -57,7 +61,7 @@ public DashboardMiddleware( public override Task Invoke(IOwinContext owinContext) { var dispatcher = _routes.FindDispatcher(owinContext.Request.Path.Value); - + if (dispatcher == null) { return Next.Invoke(owinContext); @@ -72,10 +76,11 @@ public override Task Invoke(IOwinContext owinContext) return owinContext.Response.WriteAsync("401 Unauthorized"); } } - + var context = new OwinDashboardContext( _storage, - new DashboardOptions { AppPath = _appPath, StatsPollingInterval = _statsPollingInterval, AuthorizationFilters = _authorizationFilters }, + _clock, + new DashboardOptions { AppPath = _appPath, StatsPollingInterval = _statsPollingInterval, AuthorizationFilters = _authorizationFilters }, owinContext.Environment); return dispatcher.Item1.Dispatch(context); diff --git a/src/Hangfire.Core/RecurringJobExtensions.cs b/src/Hangfire.Core/RecurringJobExtensions.cs index 76cbe7403..14ef6fd9d 100644 --- a/src/Hangfire.Core/RecurringJobExtensions.cs +++ b/src/Hangfire.Core/RecurringJobExtensions.cs @@ -1,17 +1,17 @@ // This file is part of Hangfire. // Copyright © 2019 Sergey Odinokov. -// +// // Hangfire is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation, either version 3 +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation, either version 3 // of the License, or any later version. -// +// // Hangfire is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public +// +// You should have received a copy of the GNU Lesser General Public // License along with Hangfire. If not, see . using System; @@ -94,6 +94,7 @@ public static void UpdateRecurringJob( public static BackgroundJob TriggerRecurringJob( [NotNull] this IBackgroundJobFactory factory, [NotNull] JobStorage storage, + [NotNull] IClock clock, [NotNull] IStorageConnection connection, [NotNull] IProfiler profiler, [NotNull] RecurringJobEntity recurringJob, @@ -101,11 +102,12 @@ public static BackgroundJob TriggerRecurringJob( { if (factory == null) throw new ArgumentNullException(nameof(factory)); if (storage == null) throw new ArgumentNullException(nameof(storage)); + if (clock == null) throw new ArgumentNullException(nameof(clock)); if (connection == null) throw new ArgumentNullException(nameof(connection)); if (profiler == null) throw new ArgumentNullException(nameof(profiler)); if (recurringJob == null) throw new ArgumentNullException(nameof(recurringJob)); - var context = new CreateContext(storage, connection, recurringJob.Job, null, profiler); + var context = new CreateContext(storage, clock, connection, recurringJob.Job, null, profiler); context.Parameters["RecurringJobId"] = recurringJob.RecurringJobId; context.Parameters["Time"] = JobHelper.ToTimestamp(now); @@ -120,6 +122,7 @@ public static BackgroundJob TriggerRecurringJob( public static void EnqueueBackgroundJob( [NotNull] this IStateMachine stateMachine, [NotNull] JobStorage storage, + [NotNull] IClock clock, [NotNull] IStorageConnection connection, [NotNull] IWriteOnlyTransaction transaction, [NotNull] RecurringJobEntity recurringJob, @@ -129,6 +132,7 @@ public static void EnqueueBackgroundJob( { if (stateMachine == null) throw new ArgumentNullException(nameof(stateMachine)); if (storage == null) throw new ArgumentNullException(nameof(storage)); + if (clock == null) throw new ArgumentNullException(nameof(clock)); if (connection == null) throw new ArgumentNullException(nameof(connection)); if (transaction == null) throw new ArgumentNullException(nameof(transaction)); if (recurringJob == null) throw new ArgumentNullException(nameof(recurringJob)); @@ -144,6 +148,7 @@ public static void EnqueueBackgroundJob( stateMachine.ApplyState(new ApplyStateContext( storage, + clock, connection, transaction, backgroundJob, @@ -152,4 +157,4 @@ public static void EnqueueBackgroundJob( profiler)); } } -} \ No newline at end of file +} diff --git a/src/Hangfire.Core/RecurringJobManager.cs b/src/Hangfire.Core/RecurringJobManager.cs index 4919e9891..03201145e 100644 --- a/src/Hangfire.Core/RecurringJobManager.cs +++ b/src/Hangfire.Core/RecurringJobManager.cs @@ -1,17 +1,17 @@ // This file is part of Hangfire. // Copyright © 2013-2014 Sergey Odinokov. -// +// // Hangfire is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation, either version 3 +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation, either version 3 // of the License, or any later version. -// +// // Hangfire is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public +// +// You should have received a copy of the GNU Lesser General Public // License along with Hangfire. If not, see . using System; @@ -31,62 +31,49 @@ public class RecurringJobManager : IRecurringJobManager private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(15); private readonly JobStorage _storage; + private readonly IClock _clock; private readonly IBackgroundJobFactory _factory; - private readonly Func _nowFactory; private readonly ITimeZoneResolver _timeZoneResolver; public RecurringJobManager() - : this(JobStorage.Current) + : this(JobStorage.Current, SystemClock.Current) { } - public RecurringJobManager([NotNull] JobStorage storage) - : this(storage, JobFilterProviders.Providers) + public RecurringJobManager([NotNull] JobStorage storage, [NotNull] IClock clock) + : this(storage, clock, JobFilterProviders.Providers) { } - public RecurringJobManager([NotNull] JobStorage storage, [NotNull] IJobFilterProvider filterProvider) - : this(storage, filterProvider, new DefaultTimeZoneResolver()) + public RecurringJobManager([NotNull] JobStorage storage, [NotNull] IClock clock, [NotNull] IJobFilterProvider filterProvider) + : this(storage, clock, filterProvider, new DefaultTimeZoneResolver()) { } public RecurringJobManager( - [NotNull] JobStorage storage, + [NotNull] JobStorage storage, + [NotNull] IClock clock, [NotNull] IJobFilterProvider filterProvider, [NotNull] ITimeZoneResolver timeZoneResolver) - : this(storage, filterProvider, timeZoneResolver, () => DateTime.UtcNow) + : this(storage, clock, new BackgroundJobFactory(filterProvider), timeZoneResolver) { } - public RecurringJobManager( - [NotNull] JobStorage storage, - [NotNull] IJobFilterProvider filterProvider, - [NotNull] ITimeZoneResolver timeZoneResolver, - [NotNull] Func nowFactory) - : this(storage, new BackgroundJobFactory(filterProvider), timeZoneResolver, nowFactory) - { - } - - public RecurringJobManager([NotNull] JobStorage storage, [NotNull] IBackgroundJobFactory factory) - : this(storage, factory, new DefaultTimeZoneResolver()) - { - } - - public RecurringJobManager([NotNull] JobStorage storage, [NotNull] IBackgroundJobFactory factory, [NotNull] ITimeZoneResolver timeZoneResolver) - : this(storage, factory, timeZoneResolver, () => DateTime.UtcNow) + public RecurringJobManager([NotNull] JobStorage storage, [NotNull] IClock clock, [NotNull] IBackgroundJobFactory factory) + : this(storage, clock, factory, new DefaultTimeZoneResolver()) { } - internal RecurringJobManager( - [NotNull] JobStorage storage, + public RecurringJobManager( + [NotNull] JobStorage storage, + [NotNull] IClock clock, [NotNull] IBackgroundJobFactory factory, - [NotNull] ITimeZoneResolver timeZoneResolver, - [NotNull] Func nowFactory) + [NotNull] ITimeZoneResolver timeZoneResolver) { _storage = storage ?? throw new ArgumentNullException(nameof(storage)); + _clock = clock ?? throw new ArgumentNullException(nameof(clock)); _factory = factory ?? throw new ArgumentNullException(nameof(factory)); _timeZoneResolver = timeZoneResolver ?? throw new ArgumentNullException(nameof(timeZoneResolver)); - _nowFactory = nowFactory ?? throw new ArgumentNullException(nameof(nowFactory)); } public void AddOrUpdate(string recurringJobId, Job job, string cronExpression, RecurringJobOptions options) @@ -101,7 +88,7 @@ public void AddOrUpdate(string recurringJobId, Job job, string cronExpression, R using (var connection = _storage.GetConnection()) using (connection.AcquireDistributedRecurringJobLock(recurringJobId, DefaultTimeout)) { - var recurringJob = connection.GetOrCreateRecurringJob(recurringJobId, _timeZoneResolver, _nowFactory()); + var recurringJob = connection.GetOrCreateRecurringJob(recurringJobId, _timeZoneResolver, _clock.UtcNow); recurringJob.Job = job; recurringJob.Cron = cronExpression; @@ -118,7 +105,7 @@ public void AddOrUpdate(string recurringJobId, Job job, string cronExpression, R } } } - + private static void ValidateCronExpression(string cronExpression) { try @@ -141,12 +128,12 @@ public void Trigger(string recurringJobId) using (var connection = _storage.GetConnection()) using (connection.AcquireDistributedRecurringJobLock(recurringJobId, DefaultTimeout)) { - var now = _nowFactory(); + var now = _clock.UtcNow; var recurringJob = connection.GetRecurringJob(recurringJobId, _timeZoneResolver, now); if (recurringJob == null) return; - var backgroundJob = _factory.TriggerRecurringJob(_storage, connection, EmptyProfiler.Instance, recurringJob, now); + var backgroundJob = _factory.TriggerRecurringJob(_storage, _clock, connection, EmptyProfiler.Instance, recurringJob, now); if (recurringJob.IsChanged(out var changedFields, out var nextExecution)) { @@ -156,6 +143,7 @@ public void Trigger(string recurringJobId) { _factory.StateMachine.EnqueueBackgroundJob( _storage, + _clock, connection, transaction, recurringJob, diff --git a/src/Hangfire.Core/Server/BackgroundProcessContext.cs b/src/Hangfire.Core/Server/BackgroundProcessContext.cs index 16d4441ff..640b1d435 100644 --- a/src/Hangfire.Core/Server/BackgroundProcessContext.cs +++ b/src/Hangfire.Core/Server/BackgroundProcessContext.cs @@ -1,17 +1,17 @@ // This file is part of Hangfire. // Copyright © 2013-2014 Sergey Odinokov. -// +// // Hangfire is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation, either version 3 +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation, either version 3 // of the License, or any later version. -// +// // Hangfire is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public +// +// You should have received a copy of the GNU Lesser General Public // License along with Hangfire. If not, see . using System; @@ -28,16 +28,18 @@ public class BackgroundProcessContext public BackgroundProcessContext( [NotNull] string serverId, [NotNull] JobStorage storage, + [NotNull] IClock clock, [NotNull] IDictionary properties, CancellationToken cancellationToken) - : this(serverId, storage, properties, Guid.NewGuid(), cancellationToken, cancellationToken, cancellationToken) + : this(serverId, storage, clock, properties, Guid.NewGuid(), cancellationToken, cancellationToken, cancellationToken) { } public BackgroundProcessContext( [NotNull] string serverId, - [NotNull] JobStorage storage, - [NotNull] IDictionary properties, + [NotNull] JobStorage storage, + [NotNull] IClock clock, + [NotNull] IDictionary properties, Guid executionId, CancellationToken stoppingToken, CancellationToken stoppedToken, @@ -45,25 +47,26 @@ public BackgroundProcessContext( { if (serverId == null) throw new ArgumentNullException(nameof(serverId)); if (storage == null) throw new ArgumentNullException(nameof(storage)); + if (clock == null) throw new ArgumentNullException(nameof(clock)); if (properties == null) throw new ArgumentNullException(nameof(properties)); ServerId = serverId; Storage = storage; + Clock = clock; ExecutionId = executionId; Properties = new Dictionary(properties, StringComparer.OrdinalIgnoreCase); StoppingToken = stoppingToken; StoppedToken = stoppedToken; ShutdownToken = shutdownToken; } - - [NotNull] - public string ServerId { get; } - [NotNull] - public IReadOnlyDictionary Properties { get; } + [NotNull] public string ServerId { get; } + + [NotNull] public IReadOnlyDictionary Properties { get; } + + [NotNull] public JobStorage Storage { get; } - [NotNull] - public JobStorage Storage { get; } + [NotNull] public IClock Clock { get; } public Guid ExecutionId { get; } @@ -85,4 +88,4 @@ public void Wait(TimeSpan timeout) StoppingToken.Wait(timeout); } } -} \ No newline at end of file +} diff --git a/src/Hangfire.Core/Server/BackgroundProcessDispatcherBuilder.cs b/src/Hangfire.Core/Server/BackgroundProcessDispatcherBuilder.cs index 43a950c79..89c9c7fba 100644 --- a/src/Hangfire.Core/Server/BackgroundProcessDispatcherBuilder.cs +++ b/src/Hangfire.Core/Server/BackgroundProcessDispatcherBuilder.cs @@ -1,17 +1,17 @@ // This file is part of Hangfire. // Copyright © 2017 Sergey Odinokov. -// +// // Hangfire is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation, either version 3 +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation, either version 3 // of the License, or any later version. -// +// // Hangfire is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public +// +// You should have received a copy of the GNU Lesser General Public // License along with Hangfire. If not, see . using System; @@ -72,6 +72,7 @@ private static void ExecuteProcess(Guid executionId, object state) var context = new BackgroundProcessContext( serverContext.ServerId, serverContext.Storage, + serverContext.Clock, serverContext.Properties.ToDictionary(x => x.Key, x => x.Value), executionId, serverContext.StoppingToken, diff --git a/src/Hangfire.Core/Server/BackgroundProcessDispatcherBuilderAsync.cs b/src/Hangfire.Core/Server/BackgroundProcessDispatcherBuilderAsync.cs index eaa4e87f6..b7514d7cd 100644 --- a/src/Hangfire.Core/Server/BackgroundProcessDispatcherBuilderAsync.cs +++ b/src/Hangfire.Core/Server/BackgroundProcessDispatcherBuilderAsync.cs @@ -1,17 +1,17 @@ // This file is part of Hangfire. // Copyright © 2017 Sergey Odinokov. -// +// // Hangfire is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation, either version 3 +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation, either version 3 // of the License, or any later version. -// +// // Hangfire is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public +// +// You should have received a copy of the GNU Lesser General Public // License along with Hangfire. If not, see . using System; @@ -78,11 +78,12 @@ private static async Task ExecuteProcess(Guid executionId, object state) var serverContext = tuple.Item2; var context = new BackgroundProcessContext( - serverContext.ServerId, + serverContext.ServerId, serverContext.Storage, - serverContext.Properties.ToDictionary(x => x.Key, x => x.Value), - executionId, - serverContext.StoppingToken, + serverContext.Clock, + serverContext.Properties.ToDictionary(x => x.Key, x => x.Value), + executionId, + serverContext.StoppingToken, serverContext.StoppedToken, serverContext.ShutdownToken); diff --git a/src/Hangfire.Core/Server/BackgroundProcessingServer.cs b/src/Hangfire.Core/Server/BackgroundProcessingServer.cs index 275688cda..6a0c89b25 100644 --- a/src/Hangfire.Core/Server/BackgroundProcessingServer.cs +++ b/src/Hangfire.Core/Server/BackgroundProcessingServer.cs @@ -1,17 +1,17 @@ // This file is part of Hangfire. // Copyright © 2013-2014 Sergey Odinokov. -// +// // Hangfire is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation, either version 3 +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation, either version 3 // of the License, or any later version. -// +// // Hangfire is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public +// +// You should have received a copy of the GNU Lesser General Public // License along with Hangfire. If not, see . using System; @@ -28,7 +28,7 @@ namespace Hangfire.Server /// /// Responsible for running the given collection background processes. /// - /// + /// /// /// Immediately starts the processes in a background thread. /// Responsible for announcing/removing a server, bound to a storage. @@ -58,47 +58,51 @@ public sealed class BackgroundProcessingServer : IBackgroundProcessingServer private bool _awaited; public BackgroundProcessingServer([NotNull] IEnumerable processes) - : this(JobStorage.Current, processes) + : this(JobStorage.Current, SystemClock.Current, processes) { } public BackgroundProcessingServer( [NotNull] IEnumerable processes, [NotNull] IDictionary properties) - : this(JobStorage.Current, processes, properties) + : this(JobStorage.Current, SystemClock.Current, processes, properties) { } public BackgroundProcessingServer( [NotNull] JobStorage storage, + [NotNull] IClock clock, [NotNull] IEnumerable processes) - : this(storage, processes, new Dictionary()) + : this(storage, clock, processes, new Dictionary()) { } public BackgroundProcessingServer( [NotNull] JobStorage storage, + [NotNull] IClock clock, [NotNull] IEnumerable processes, [NotNull] IDictionary properties) - : this(storage, processes, properties, new BackgroundProcessingServerOptions()) + : this(storage, clock, processes, properties, new BackgroundProcessingServerOptions()) { } public BackgroundProcessingServer( [NotNull] JobStorage storage, + [NotNull] IClock clock, [NotNull] IEnumerable processes, [NotNull] IDictionary properties, [NotNull] BackgroundProcessingServerOptions options) - : this(storage, GetProcesses(processes), properties, options) + : this(storage, clock, GetProcesses(processes), properties, options) { } public BackgroundProcessingServer( [NotNull] JobStorage storage, + [NotNull] IClock clock, [NotNull] IEnumerable dispatcherBuilders, [NotNull] IDictionary properties, [NotNull] BackgroundProcessingServerOptions options) - : this(new BackgroundServerProcess(storage, dispatcherBuilders, options, properties), options) + : this(new BackgroundServerProcess(storage, clock, dispatcherBuilders, options, properties), options) { } @@ -213,7 +217,7 @@ private IBackgroundDispatcher CreateDispatcher() private void RunServer(Guid executionId, object state) { - _process.Execute(executionId, (BackgroundExecution)state, _stoppingCts.Token, _stoppedCts.Token, _shutdownCts.Token); + _process.Execute(executionId, (BackgroundExecution) state, _stoppingCts.Token, _stoppedCts.Token, _shutdownCts.Token); } private static IEnumerable ThreadFactory(ThreadStart threadStart) diff --git a/src/Hangfire.Core/Server/BackgroundServerContext.cs b/src/Hangfire.Core/Server/BackgroundServerContext.cs index 571c75aa2..bd0f8a7a7 100644 --- a/src/Hangfire.Core/Server/BackgroundServerContext.cs +++ b/src/Hangfire.Core/Server/BackgroundServerContext.cs @@ -1,17 +1,17 @@ // This file is part of Hangfire. // Copyright © 2018 Sergey Odinokov. -// +// // Hangfire is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation, either version 3 +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation, either version 3 // of the License, or any later version. -// +// // Hangfire is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public +// +// You should have received a copy of the GNU Lesser General Public // License along with Hangfire. If not, see . using System; @@ -26,13 +26,15 @@ public class BackgroundServerContext public BackgroundServerContext( [NotNull] string serverId, [NotNull] JobStorage storage, - [NotNull] IDictionary properties, - CancellationToken stoppingToken, + [NotNull] IClock clock, + [NotNull] IDictionary properties, + CancellationToken stoppingToken, CancellationToken stoppedToken, CancellationToken shutdownToken) { ServerId = serverId ?? throw new ArgumentNullException(nameof(serverId)); Storage = storage ?? throw new ArgumentNullException(nameof(storage)); + Clock = clock ?? throw new ArgumentNullException(nameof(clock)); Properties = properties ?? throw new ArgumentNullException(nameof(properties)); StoppingToken = stoppingToken; StoppedToken = stoppedToken; @@ -41,6 +43,7 @@ public BackgroundServerContext( public string ServerId { get; } public JobStorage Storage { get; } + public IClock Clock { get; } public IDictionary Properties { get; } public CancellationToken StoppingToken { get; } public CancellationToken StoppedToken { get; } diff --git a/src/Hangfire.Core/Server/BackgroundServerProcess.cs b/src/Hangfire.Core/Server/BackgroundServerProcess.cs index 4e302b636..a1501ffaa 100644 --- a/src/Hangfire.Core/Server/BackgroundServerProcess.cs +++ b/src/Hangfire.Core/Server/BackgroundServerProcess.cs @@ -1,17 +1,17 @@ // This file is part of Hangfire. // Copyright © 2017 Sergey Odinokov. -// +// // Hangfire is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation, either version 3 +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation, either version 3 // of the License, or any later version. -// +// // Hangfire is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public +// +// You should have received a copy of the GNU Lesser General Public // License along with Hangfire. If not, see . using System; @@ -30,19 +30,23 @@ internal sealed class BackgroundServerProcess : IBackgroundServerProcess { private readonly ILog _logger = LogProvider.GetLogger(typeof(BackgroundServerProcess)); private readonly JobStorage _storage; + private readonly IClock _clock; private readonly BackgroundProcessingServerOptions _options; private readonly IDictionary _properties; private readonly IBackgroundProcessDispatcherBuilder[] _dispatcherBuilders; public BackgroundServerProcess( [NotNull] JobStorage storage, + [NotNull] IClock clock, [NotNull] IEnumerable dispatcherBuilders, [NotNull] BackgroundProcessingServerOptions options, [NotNull] IDictionary properties) { + if (clock == null) throw new ArgumentNullException(nameof(clock)); if (dispatcherBuilders == null) throw new ArgumentNullException(nameof(dispatcherBuilders)); _storage = storage ?? throw new ArgumentNullException(nameof(storage)); + _clock = clock; _options = options ?? throw new ArgumentNullException(nameof(options)); _properties = properties ?? throw new ArgumentNullException(nameof(properties)); @@ -87,6 +91,7 @@ void HandleRestartSignal() var context = new BackgroundServerContext( serverId, _storage, + _clock, _properties, restartStoppingCts.Token, restartStoppedCts.Token, @@ -133,7 +138,7 @@ private IEnumerable GetRequiredProcesses() private IEnumerable GetStorageComponents() { return _storage.GetComponents().Select(component => new ServerProcessDispatcherBuilder( - component, + component, threadStart => BackgroundProcessExtensions.DefaultThreadFactory(1, component.GetType().Name, threadStart))); } diff --git a/src/Hangfire.Core/Server/DelayedJobScheduler.cs b/src/Hangfire.Core/Server/DelayedJobScheduler.cs index 46629c5ff..792f9d57b 100644 --- a/src/Hangfire.Core/Server/DelayedJobScheduler.cs +++ b/src/Hangfire.Core/Server/DelayedJobScheduler.cs @@ -1,17 +1,17 @@ // This file is part of Hangfire. // Copyright © 2013-2014 Sergey Odinokov. -// +// // Hangfire is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation, either version 3 +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation, either version 3 // of the License, or any later version. -// +// // Hangfire is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public +// +// You should have received a copy of the GNU Lesser General Public // License along with Hangfire. If not, see . using System; @@ -30,41 +30,41 @@ namespace Hangfire.Server /// Represents a background process responsible for enqueueing delayed /// jobs. /// - /// + /// /// - /// This background process polls the delayed job schedule for + /// This background process polls the delayed job schedule for /// delayed jobs that are ready to be enqueued. To prevent a stress load - /// on a job storage, the configurable delay is used between scheduler + /// on a job storage, the configurable delay is used between scheduler /// runs. Delay is used only when there are no more background jobs to be /// enqueued. - /// + /// /// When a background job is ready to be enqueued, it is simply /// moved from to the /// by using . - /// + /// /// Delayed job schedule is based on a Set data structure of a job /// storage, so you can use this background process as an example of a /// custom extension. - /// + /// /// Multiple instances of this background process can be used in /// separate threads/processes without additional configuration (distributed - /// locks are used). However, this only adds support for fail-over, and does + /// locks are used). However, this only adds support for fail-over, and does /// not increase the performance. - /// + /// /// /// If you are using custom filter providers, you need to pass a custom /// instance to make this process /// respect your filters when enqueueing background jobs. /// /// - /// + /// /// - /// + /// /// public class DelayedJobScheduler : IBackgroundProcess { /// - /// Represents a default polling interval for delayed job scheduler. + /// Represents a default polling interval for delayed job scheduler. /// This field is read-only. /// /// @@ -86,7 +86,7 @@ public class DelayedJobScheduler : IBackgroundProcess /// class with the value as a /// delay between runs. /// - public DelayedJobScheduler() + public DelayedJobScheduler() : this(DefaultPollingDelay) { } @@ -107,7 +107,7 @@ public DelayedJobScheduler(TimeSpan pollingDelay) /// /// Delay between scheduler runs. /// State changer to use for background jobs. - /// + /// /// is null. public DelayedJobScheduler(TimeSpan pollingDelay, [NotNull] IBackgroundJobStateChanger stateChanger) { @@ -155,7 +155,7 @@ private bool EnqueueNextScheduledJobs(BackgroundProcessContext context) { if (IsBatchingAvailable(connection)) { - var timestamp = JobHelper.ToTimestamp(DateTime.UtcNow); + var timestamp = JobHelper.ToTimestamp(context.Clock.UtcNow); var jobIds = ((JobStorageConnection)connection).GetFirstByLowestScoreFromSet("schedule", 0, timestamp, BatchSize); if (jobIds == null || jobIds.Count == 0) return false; @@ -172,7 +172,7 @@ private bool EnqueueNextScheduledJobs(BackgroundProcessContext context) { if (context.IsStopping) return false; - var timestamp = JobHelper.ToTimestamp(DateTime.UtcNow); + var timestamp = JobHelper.ToTimestamp(context.Clock.UtcNow); var jobId = connection.GetFirstByLowestScoreFromSet("schedule", 0, timestamp); if (jobId == null) return false; @@ -189,6 +189,7 @@ private void EnqueueBackgroundJob(BackgroundProcessContext context, IStorageConn { var appliedState = _stateChanger.ChangeState(new StateChangeContext( context.Storage, + context.Clock, connection, jobId, new EnqueuedState { Reason = $"Triggered by {ToString()}" }, @@ -252,10 +253,10 @@ private T UseConnectionDistributedLock(JobStorage storage, Func. using System; @@ -26,40 +26,40 @@ namespace Hangfire.Server { /// - /// Represents a background process responsible for enqueueing recurring + /// Represents a background process responsible for enqueueing recurring /// jobs. /// - /// + /// /// /// This background process polls the recurring job schedule /// for recurring jobs ready to be enqueued. Interval between scheduler /// polls is hard-coded to 1 minute as a compromise between /// frequency and additional stress on job storage. - /// + /// /// /// Use custom background processes if you need to schedule recurring jobs - /// with frequency less than one minute. Please see the + /// with frequency less than one minute. Please see the /// interface for details. /// - /// + /// /// Recurring job schedule is based on Set and Hash data structures - /// of a job storage, so you can use this background process as an example + /// of a job storage, so you can use this background process as an example /// of a custom extension. - /// + /// /// Multiple instances of this background process can be used in /// separate threads/processes without additional configuration (distributed - /// locks are used). However, this only adds support for fail-over, and does + /// locks are used). However, this only adds support for fail-over, and does /// not increase the performance. - /// + /// /// - /// If you are using custom filter providers, you need to pass a - /// custom instance to make this + /// If you are using custom filter providers, you need to pass a + /// custom instance to make this /// process respect your filters when enqueueing background jobs. /// /// - /// + /// /// - /// + /// /// public class RecurringJobScheduler : IBackgroundProcess { @@ -70,7 +70,6 @@ public class RecurringJobScheduler : IBackgroundProcess private readonly ConcurrentDictionary _isBatchingAvailableCache = new ConcurrentDictionary(); private readonly IBackgroundJobFactory _factory; - private readonly Func _nowFactory; private readonly ITimeZoneResolver _timeZoneResolver; private readonly TimeSpan _pollingDelay; private readonly IProfiler _profiler; @@ -83,13 +82,13 @@ public RecurringJobScheduler() : this(new BackgroundJobFactory(JobFilterProviders.Providers)) { } - + /// /// Initializes a new instance of the /// class with custom background job factory and a state machine. /// /// Factory that will be used to create background jobs. - /// + /// /// is null. public RecurringJobScheduler( [NotNull] IBackgroundJobFactory factory) @@ -124,22 +123,11 @@ public RecurringJobScheduler( [NotNull] IBackgroundJobFactory factory, TimeSpan pollingDelay, [NotNull] ITimeZoneResolver timeZoneResolver) - : this(factory, pollingDelay, timeZoneResolver, () => DateTime.UtcNow) - { - } - - public RecurringJobScheduler( - [NotNull] IBackgroundJobFactory factory, - TimeSpan pollingDelay, - [NotNull] ITimeZoneResolver timeZoneResolver, - [NotNull] Func nowFactory) { if (factory == null) throw new ArgumentNullException(nameof(factory)); - if (nowFactory == null) throw new ArgumentNullException(nameof(nowFactory)); if (timeZoneResolver == null) throw new ArgumentNullException(nameof(timeZoneResolver)); _factory = factory; - _nowFactory = nowFactory; _timeZoneResolver = timeZoneResolver; _pollingDelay = pollingDelay; _profiler = new SlowLogProfiler(_logger); @@ -173,7 +161,7 @@ public void Execute(BackgroundProcessContext context) } else { - var now = _nowFactory(); + var now = context.Clock.UtcNow; context.Wait(now.AddMilliseconds(-now.Millisecond).AddSeconds(-now.Second).AddMinutes(1) - now); } } @@ -192,9 +180,9 @@ private bool EnqueueNextRecurringJobs(BackgroundProcessContext context) if (IsBatchingAvailable(connection)) { - var now = _nowFactory(); + var now = context.Clock.UtcNow; var timestamp = JobHelper.ToTimestamp(now); - var recurringJobIds = ((JobStorageConnection)connection).GetFirstByLowestScoreFromSet("recurring-jobs", 0, timestamp, BatchSize); + var recurringJobIds = ((JobStorageConnection) connection).GetFirstByLowestScoreFromSet("recurring-jobs", 0, timestamp, BatchSize); if (recurringJobIds == null || recurringJobIds.Count == 0) return false; @@ -214,7 +202,7 @@ private bool EnqueueNextRecurringJobs(BackgroundProcessContext context) { if (context.IsStopping) return false; - var now = _nowFactory(); + var now = context.Clock.UtcNow; var timestamp = JobHelper.ToTimestamp(now); var recurringJobId = connection.GetFirstByLowestScoreFromSet("recurring-jobs", 0, timestamp); @@ -253,7 +241,7 @@ private bool TryEnqueueBackgroundJob( private bool EnqueueBackgroundJob( BackgroundProcessContext context, - IStorageConnection connection, + IStorageConnection connection, string recurringJobId, DateTime now) { @@ -289,7 +277,7 @@ private bool EnqueueBackgroundJob( if (nextExecution.HasValue && nextExecution <= now) { - backgroundJob = _factory.TriggerRecurringJob(context.Storage, connection, _profiler, recurringJob, now); + backgroundJob = _factory.TriggerRecurringJob(context.Storage, context.Clock, connection, _profiler, recurringJob, now); if (String.IsNullOrEmpty(backgroundJob?.Id)) { @@ -309,6 +297,7 @@ private bool EnqueueBackgroundJob( { _factory.StateMachine.EnqueueBackgroundJob( context.Storage, + context.Clock, connection, transaction, recurringJob, @@ -359,7 +348,8 @@ private bool UseConnectionDistributedLock(JobStorage storage, Func $@"An exception was thrown during acquiring distributed lock the {resource} resource within {LockTimeout.TotalSeconds} seconds. The recurring jobs have not been handled this time.", + () => + $@"An exception was thrown during acquiring distributed lock the {resource} resource within {LockTimeout.TotalSeconds} seconds. The recurring jobs have not been handled this time.", e); } @@ -392,4 +382,4 @@ private bool IsBatchingAvailable(IStorageConnection connection) }); } } -} \ No newline at end of file +} diff --git a/src/Hangfire.Core/Server/Worker.cs b/src/Hangfire.Core/Server/Worker.cs index c46e2568c..529a31723 100644 --- a/src/Hangfire.Core/Server/Worker.cs +++ b/src/Hangfire.Core/Server/Worker.cs @@ -1,17 +1,17 @@ // This file is part of Hangfire. -// Copyright © 2013-2014 Sergey Odinokov. -// +// Copyright � 2013-2014 Sergey Odinokov. +// // Hangfire is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation, either version 3 +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation, either version 3 // of the License, or any later version. -// +// // Hangfire is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public +// +// You should have received a copy of the GNU Lesser General Public // License along with Hangfire. If not, see . using System; @@ -29,16 +29,16 @@ namespace Hangfire.Server { /// - /// Represents a background process responsible for processing + /// Represents a background process responsible for processing /// fire-and-forget jobs. /// - /// + /// /// /// This is the heart of background processing in Hangfire /// - /// + /// /// - /// + /// /// public class Worker : IBackgroundProcess { @@ -52,7 +52,7 @@ public class Worker : IBackgroundProcess private readonly IBackgroundJobPerformer _performer; private readonly IBackgroundJobStateChanger _stateChanger; private readonly IProfiler _profiler; - + public Worker() : this(EnqueuedState.DefaultQueue) { } @@ -72,7 +72,7 @@ public Worker( internal Worker( [NotNull] IEnumerable queues, - [NotNull] IBackgroundJobPerformer performer, + [NotNull] IBackgroundJobPerformer performer, [NotNull] IBackgroundJobStateChanger stateChanger, TimeSpan jobInitializationTimeout, int maxStateChangeAttempts) @@ -80,7 +80,7 @@ internal Worker( if (queues == null) throw new ArgumentNullException(nameof(queues)); if (performer == null) throw new ArgumentNullException(nameof(performer)); if (stateChanger == null) throw new ArgumentNullException(nameof(stateChanger)); - + _queues = queues.ToArray(); _performer = performer; _stateChanger = stateChanger; @@ -111,10 +111,10 @@ public void Execute(BackgroundProcessContext context) var processingState = new ProcessingState(context.ServerId, context.ExecutionId.ToString()); var appliedState = TryChangeState( - context, - connection, - fetchedJob, - processingState, + context, + connection, + fetchedJob, + processingState, new[] { EnqueuedState.StateName, ProcessingState.StateName }, linkedCts.Token, context.StoppingToken); @@ -173,8 +173,8 @@ public void Execute(BackgroundProcessContext context) } private IState TryChangeState( - BackgroundProcessContext context, - IStorageConnection connection, + BackgroundProcessContext context, + IStorageConnection connection, IFetchedJob fetchedJob, IState state, string[] expectedStates, @@ -191,6 +191,7 @@ private IState TryChangeState( { return _stateChanger.ChangeState(new StateChangeContext( context.Storage, + context.Clock, connection, fetchedJob.JobId, state, @@ -201,7 +202,7 @@ private IState TryChangeState( catch (Exception ex) { _logger.DebugException( - $"State change attempt {retryAttempt + 1} of {_maxStateChangeAttempts} failed due to an error, see inner exception for details", + $"State change attempt {retryAttempt + 1} of {_maxStateChangeAttempts} failed due to an error, see inner exception for details", ex); exception = ex; @@ -217,6 +218,7 @@ private IState TryChangeState( return _stateChanger.ChangeState(new StateChangeContext( context.Storage, + context.Clock, connection, fetchedJob.JobId, new FailedState(exception) { Reason = $"Failed to change state to a '{state.Name}' one due to an exception after {_maxStateChangeAttempts} retry attempts" }, @@ -259,7 +261,7 @@ private IState PerformJob(BackgroundProcessContext context, IStorageConnection c { var performContext = new PerformContext(context.Storage, connection, backgroundJob, jobToken, _profiler); - var latency = (DateTime.UtcNow - jobData.CreatedAt).TotalMilliseconds; + var latency = (context.Clock.UtcNow - jobData.CreatedAt).TotalMilliseconds; var duration = Stopwatch.StartNew(); var result = _performer.Perform(performContext); @@ -296,4 +298,4 @@ private IState PerformJob(BackgroundProcessContext context, IStorageConnection c } } } -} \ No newline at end of file +} diff --git a/src/Hangfire.Core/States/ApplyStateContext.cs b/src/Hangfire.Core/States/ApplyStateContext.cs index df1d17ddf..fd7940b06 100644 --- a/src/Hangfire.Core/States/ApplyStateContext.cs +++ b/src/Hangfire.Core/States/ApplyStateContext.cs @@ -1,17 +1,17 @@ // This file is part of Hangfire. // Copyright © 2013-2014 Sergey Odinokov. -// +// // Hangfire is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation, either version 3 +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation, either version 3 // of the License, or any later version. -// +// // Hangfire is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public +// +// You should have received a copy of the GNU Lesser General Public // License along with Hangfire. If not, see . using System; @@ -26,41 +26,45 @@ public class ApplyStateContext : StateContext #pragma warning restore 618 { public ApplyStateContext( - [NotNull] IWriteOnlyTransaction transaction, + [NotNull] IWriteOnlyTransaction transaction, [NotNull] ElectStateContext context) - : this(context.Storage, context.Connection, transaction, context.BackgroundJob, context.CandidateState, context.CurrentState, context.Profiler) + : this(context.Storage, context.Clock, context.Connection, transaction, context.BackgroundJob, context.CandidateState, context.CurrentState, context.Profiler) { } public ApplyStateContext( [NotNull] JobStorage storage, + [NotNull] IClock clock, [NotNull] IStorageConnection connection, [NotNull] IWriteOnlyTransaction transaction, [NotNull] BackgroundJob backgroundJob, [NotNull] IState newState, [CanBeNull] string oldStateName) - : this(storage, connection, transaction, backgroundJob, newState, oldStateName, EmptyProfiler.Instance) + : this(storage, clock, connection, transaction, backgroundJob, newState, oldStateName, EmptyProfiler.Instance) { } internal ApplyStateContext( [NotNull] JobStorage storage, + [NotNull] IClock clock, [NotNull] IStorageConnection connection, [NotNull] IWriteOnlyTransaction transaction, [NotNull] BackgroundJob backgroundJob, - [NotNull] IState newState, + [NotNull] IState newState, [CanBeNull] string oldStateName, [NotNull] IProfiler profiler) { if (storage == null) throw new ArgumentNullException(nameof(storage)); + if (clock == null) throw new ArgumentNullException(nameof(clock)); if (connection == null) throw new ArgumentNullException(nameof(connection)); if (transaction == null) throw new ArgumentNullException(nameof(transaction)); if (backgroundJob == null) throw new ArgumentNullException(nameof(backgroundJob)); if (newState == null) throw new ArgumentNullException(nameof(newState)); - + BackgroundJob = backgroundJob; Storage = storage; + Clock = clock; Connection = connection; Transaction = transaction; OldStateName = oldStateName; @@ -69,26 +73,22 @@ internal ApplyStateContext( Profiler = profiler; } - [NotNull] - public JobStorage Storage { get; } + [NotNull] public JobStorage Storage { get; } - [NotNull] - public IStorageConnection Connection { get; } + [NotNull] public IClock Clock { get; } + + [NotNull] public IStorageConnection Connection { get; } + + [NotNull] public IWriteOnlyTransaction Transaction { get; } - [NotNull] - public IWriteOnlyTransaction Transaction { get; } - public override BackgroundJob BackgroundJob { get; } - [CanBeNull] - public string OldStateName { get; } + [CanBeNull] public string OldStateName { get; } + + [NotNull] public IState NewState { get; } - [NotNull] - public IState NewState { get; } - public TimeSpan JobExpirationTimeout { get; set; } - [NotNull] - internal IProfiler Profiler { get; } + [NotNull] internal IProfiler Profiler { get; } } -} \ No newline at end of file +} diff --git a/src/Hangfire.Core/States/AwaitingState.cs b/src/Hangfire.Core/States/AwaitingState.cs index 92966bd20..919a7da8c 100644 --- a/src/Hangfire.Core/States/AwaitingState.cs +++ b/src/Hangfire.Core/States/AwaitingState.cs @@ -1,17 +1,17 @@ // This file is part of Hangfire. // Copyright © 2013-2014 Sergey Odinokov. -// +// // Hangfire is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation, either version 3 +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation, either version 3 // of the License, or any later version. -// +// // Hangfire is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public +// +// You should have received a copy of the GNU Lesser General Public // License along with Hangfire. If not, see . using System; @@ -29,12 +29,12 @@ namespace Hangfire.States /// by the /// filter. /// - /// + /// /// /// Background job in is referred as a /// continuation of a background job with . /// - /// + /// /// public class AwaitingState : IState { @@ -47,10 +47,10 @@ public class AwaitingState : IState /// The value of this field is "Awaiting". /// public static readonly string StateName = "Awaiting"; - + /// /// Initializes a new instance of the class with - /// the specified parent background job id and with an instance of the + /// the specified parent background job id and with an instance of the /// class as a next state. /// /// The identifier of a background job to wait for. @@ -163,7 +163,7 @@ public AwaitingState( /// /// - /// Returning dictionary contains the following keys. You can obtain + /// Returning dictionary contains the following keys. You can obtain /// the state data by using the /// method. /// @@ -183,7 +183,7 @@ public AwaitingState( /// NextState /// /// - /// with + /// with /// /// /// Please see the property. @@ -223,7 +223,7 @@ internal class Handler : IStateHandler { public void Apply(ApplyStateContext context, IWriteOnlyTransaction transaction) { - transaction.AddToSet("awaiting", context.BackgroundJob.Id, JobHelper.ToTimestamp(DateTime.UtcNow)); + transaction.AddToSet("awaiting", context.BackgroundJob.Id, JobHelper.ToTimestamp(context.Clock.UtcNow)); } public void Unapply(ApplyStateContext context, IWriteOnlyTransaction transaction) @@ -235,4 +235,4 @@ public void Unapply(ApplyStateContext context, IWriteOnlyTransaction transaction public string StateName => AwaitingState.StateName; } } -} \ No newline at end of file +} diff --git a/src/Hangfire.Core/States/BackgroundJobStateChanger.cs b/src/Hangfire.Core/States/BackgroundJobStateChanger.cs index 37a8f70a5..4725a897f 100644 --- a/src/Hangfire.Core/States/BackgroundJobStateChanger.cs +++ b/src/Hangfire.Core/States/BackgroundJobStateChanger.cs @@ -1,17 +1,17 @@ // This file is part of Hangfire. // Copyright © 2013-2014 Sergey Odinokov. -// +// // Hangfire is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation, either version 3 +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation, either version 3 // of the License, or any later version. -// +// // Hangfire is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public +// +// You should have received a copy of the GNU Lesser General Public // License along with Hangfire. If not, see . using System; @@ -45,7 +45,7 @@ internal BackgroundJobStateChanger([NotNull] IJobFilterProvider filterProvider, _stateMachine = new StateMachine(filterProvider, stateMachine); } - + public IState ChangeState(StateChangeContext context) { // To ensure that job state will be changed only from one of the @@ -67,7 +67,7 @@ public IState ChangeState(StateChangeContext context) { return null; } - + var stateToApply = context.NewState; try @@ -80,7 +80,7 @@ public IState ChangeState(StateChangeContext context) // serialized within a background job. There are many reasons // for this case, including refactored code, or a missing // assembly reference due to a mistake or erroneous deployment. - // + // // The problem is that in this case we can't get any filters, // applied at a method or a class level, and we can't proceed // with the state change without breaking a consistent behavior: @@ -91,11 +91,11 @@ public IState ChangeState(StateChangeContext context) // There's a problem with filters related to handling the states // which ignore this exception, i.e. fitlers for the FailedState // and the DeletedState, such as AutomaticRetryAttrubute filter. - // + // // We should document that such a filters may not be fired, when // we can't find a target method, and these filters should be // applied only at the global level to get consistent results. - // + // // In 2.0 we should have a special state for all the errors, when // Hangfire doesn't know what to do, without any possibility to // add method or class-level filters for such a state to provide @@ -114,6 +114,7 @@ public IState ChangeState(StateChangeContext context) { var applyContext = new ApplyStateContext( context.Storage, + context.Clock, context.Connection, transaction, new BackgroundJob(context.BackgroundJobId, jobData.Job, jobData.CreatedAt), @@ -144,14 +145,14 @@ private static JobData GetJobData(StateChangeContext context) // the NULL value instead of waiting for a transaction to be committed. With // this code, we will make several retry attempts to handle this case to wait // on the client side. - // + // // On the other hand, we need to give up after some retry attempt, because // we should also handle the case, when our queue and job storage became // unsynchronized with each other due to failures, manual intervention or so. // Otherwise we will wait forever in this cases, since And there's no way to // make a distinction between a non-linearizable read and the storages, non- // synchronized with each other. - // + // // In recent versions, Hangfire.SqlServer uses query hints to make all the // reads linearizable no matter what, but there may be other storages that // still require this workaround. @@ -190,8 +191,8 @@ private static JobData GetJobData(StateChangeContext context) // There is always a chance it will be issued against a non-existing or an // already expired background job, and a minute wait (or whatever timeout is // used) is completely unnecessary in this case. - // - // Since waiting is only required when a worker picks up a job, and + // + // Since waiting is only required when a worker picks up a job, and // cancellation tokens are used only by the Worker class, we can avoid the // unnecessary waiting logic when no cancellation token is passed. diff --git a/src/Hangfire.Core/States/ElectStateContext.cs b/src/Hangfire.Core/States/ElectStateContext.cs index 4d4bafdce..958bb14d3 100644 --- a/src/Hangfire.Core/States/ElectStateContext.cs +++ b/src/Hangfire.Core/States/ElectStateContext.cs @@ -1,17 +1,17 @@ // This file is part of Hangfire. // Copyright © 2013-2014 Sergey Odinokov. -// +// // Hangfire is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation, either version 3 +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation, either version 3 // of the License, or any later version. -// +// // Hangfire is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public +// +// You should have received a copy of the GNU Lesser General Public // License along with Hangfire. If not, see . using System; @@ -34,7 +34,7 @@ public class ElectStateContext : StateContext public ElectStateContext([NotNull] ApplyStateContext applyContext) { if (applyContext == null) throw new ArgumentNullException(nameof(applyContext)); - + BackgroundJob = applyContext.BackgroundJob; _candidateState = applyContext.NewState; @@ -43,13 +43,17 @@ public ElectStateContext([NotNull] ApplyStateContext applyContext) Transaction = applyContext.Transaction; CurrentState = applyContext.OldStateName; Profiler = applyContext.Profiler; + Clock = applyContext.Clock; } - + public override BackgroundJob BackgroundJob { get; } [NotNull] public JobStorage Storage { get; } + [NotNull] + public IClock Clock { get; } + [NotNull] public IStorageConnection Connection { get; } @@ -96,4 +100,4 @@ public T GetJobParameter(string name) SerializationOption.User); } } -} \ No newline at end of file +} diff --git a/src/Hangfire.Core/States/StateChangeContext.cs b/src/Hangfire.Core/States/StateChangeContext.cs index fa3806cee..5c37d31a0 100644 --- a/src/Hangfire.Core/States/StateChangeContext.cs +++ b/src/Hangfire.Core/States/StateChangeContext.cs @@ -1,17 +1,17 @@ // This file is part of Hangfire. -// Copyright © 2013-2014 Sergey Odinokov. -// +// Copyright � 2013-2014 Sergey Odinokov. +// // Hangfire is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation, either version 3 +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation, either version 3 // of the License, or any later version. -// +// // Hangfire is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public +// +// You should have received a copy of the GNU Lesser General Public // License along with Hangfire. If not, see . using System; @@ -27,50 +27,56 @@ public class StateChangeContext { public StateChangeContext( [NotNull] JobStorage storage, + [NotNull] IClock clock, [NotNull] IStorageConnection connection, - [NotNull] string backgroundJobId, + [NotNull] string backgroundJobId, [NotNull] IState newState) - : this(storage, connection, backgroundJobId, newState, null) + : this(storage, clock, connection, backgroundJobId, newState, null) { } public StateChangeContext( [NotNull] JobStorage storage, + [NotNull] IClock clock, [NotNull] IStorageConnection connection, - [NotNull] string backgroundJobId, - [NotNull] IState newState, + [NotNull] string backgroundJobId, + [NotNull] IState newState, [CanBeNull] params string[] expectedStates) - : this(storage, connection, backgroundJobId, newState, expectedStates, CancellationToken.None) + : this(storage, clock, connection, backgroundJobId, newState, expectedStates, CancellationToken.None) { } public StateChangeContext( [NotNull] JobStorage storage, + [NotNull] IClock clock, [NotNull] IStorageConnection connection, [NotNull] string backgroundJobId, [NotNull] IState newState, [CanBeNull] IEnumerable expectedStates, CancellationToken cancellationToken) - : this(storage, connection, backgroundJobId, newState, expectedStates, cancellationToken, EmptyProfiler.Instance) + : this(storage, clock, connection, backgroundJobId, newState, expectedStates, cancellationToken, EmptyProfiler.Instance) { } internal StateChangeContext( - [NotNull] JobStorage storage, + [NotNull] JobStorage storage, + [NotNull] IClock clock, [NotNull] IStorageConnection connection, - [NotNull] string backgroundJobId, - [NotNull] IState newState, - [CanBeNull] IEnumerable expectedStates, + [NotNull] string backgroundJobId, + [NotNull] IState newState, + [CanBeNull] IEnumerable expectedStates, CancellationToken cancellationToken, [NotNull] IProfiler profiler) { if (storage == null) throw new ArgumentNullException(nameof(storage)); + if (clock == null) throw new ArgumentNullException(nameof(clock)); if (connection == null) throw new ArgumentNullException(nameof(connection)); if (backgroundJobId == null) throw new ArgumentNullException(nameof(backgroundJobId)); if (newState == null) throw new ArgumentNullException(nameof(newState)); if (profiler == null) throw new ArgumentNullException(nameof(profiler)); Storage = storage; + Clock = clock; Connection = connection; BackgroundJobId = backgroundJobId; NewState = newState; @@ -80,6 +86,7 @@ internal StateChangeContext( } public JobStorage Storage { get; } + public IClock Clock { get; } public IStorageConnection Connection { get; } public string BackgroundJobId { get; } public IState NewState { get; } @@ -87,4 +94,4 @@ internal StateChangeContext( public CancellationToken CancellationToken { get; } internal IProfiler Profiler { get; } } -} \ No newline at end of file +} diff --git a/src/Hangfire.Core/StatisticsHistoryAttribute.cs b/src/Hangfire.Core/StatisticsHistoryAttribute.cs index 335c0e943..9b6944680 100644 --- a/src/Hangfire.Core/StatisticsHistoryAttribute.cs +++ b/src/Hangfire.Core/StatisticsHistoryAttribute.cs @@ -1,17 +1,17 @@ // This file is part of Hangfire. // Copyright © 2013-2014 Sergey Odinokov. -// +// // Hangfire is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation, either version 3 +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation, either version 3 // of the License, or any later version. -// +// // Hangfire is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public +// +// You should have received a copy of the GNU Lesser General Public // License along with Hangfire. If not, see . using System; @@ -33,21 +33,21 @@ public void OnStateElection(ElectStateContext context) if (context.CandidateState.Name == SucceededState.StateName) { context.Transaction.IncrementCounter( - $"stats:succeeded:{DateTime.UtcNow.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)}", - DateTime.UtcNow.AddMonths(1) - DateTime.UtcNow); + $"stats:succeeded:{context.Clock.UtcNow.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)}", + context.Clock.UtcNow.AddMonths(1) - context.Clock.UtcNow); context.Transaction.IncrementCounter( - $"stats:succeeded:{DateTime.UtcNow.ToString("yyyy-MM-dd-HH")}", + $"stats:succeeded:{context.Clock.UtcNow.ToString("yyyy-MM-dd-HH")}", TimeSpan.FromDays(1)); } else if (context.CandidateState.Name == FailedState.StateName) { context.Transaction.IncrementCounter( - $"stats:failed:{DateTime.UtcNow.ToString("yyyy-MM-dd")}", - DateTime.UtcNow.AddMonths(1) - DateTime.UtcNow); + $"stats:failed:{context.Clock.UtcNow.ToString("yyyy-MM-dd")}", + context.Clock.UtcNow.AddMonths(1) - context.Clock.UtcNow); context.Transaction.IncrementCounter( - $"stats:failed:{DateTime.UtcNow.ToString("yyyy-MM-dd-HH")}", + $"stats:failed:{context.Clock.UtcNow.ToString("yyyy-MM-dd-HH")}", TimeSpan.FromDays(1)); } } diff --git a/tests/Hangfire.Core.Tests/BackgroundJobClientFacts.cs b/tests/Hangfire.Core.Tests/BackgroundJobClientFacts.cs index 81c72d1b5..242bab9f5 100644 --- a/tests/Hangfire.Core.Tests/BackgroundJobClientFacts.cs +++ b/tests/Hangfire.Core.Tests/BackgroundJobClientFacts.cs @@ -19,6 +19,7 @@ public class BackgroundJobClientFacts private readonly Mock _state; private readonly Job _job; private readonly Mock _stateChanger; + private readonly Mock _clock; public BackgroundJobClientFacts() { @@ -26,8 +27,10 @@ public BackgroundJobClientFacts() _storage = new Mock(); _storage.Setup(x => x.GetConnection()).Returns(connection.Object); + _clock = new Mock(); + _stateChanger = new Mock(); - + _state = new Mock(); _state.Setup(x => x.Name).Returns("Mock"); _job = Job.FromExpression(() => Method()); @@ -41,16 +44,25 @@ public BackgroundJobClientFacts() public void Ctor_ThrowsAnException_WhenStorageIsNull() { var exception = Assert.Throws( - () => new BackgroundJobClient(null, _factory.Object, _stateChanger.Object)); + () => new BackgroundJobClient(null, _clock.Object, _factory.Object, _stateChanger.Object)); Assert.Equal("storage", exception.ParamName); } + [Fact] + public void Ctor_ThrowsAnException_WhenClockIsNull() + { + var exception = Assert.Throws( + () => new BackgroundJobClient(_storage.Object, null, _factory.Object, _stateChanger.Object)); + + Assert.Equal("clock", exception.ParamName); + } + [Fact] public void Ctor_ThrowsAnException_WhenFactoryIsNull() { var exception = Assert.Throws( - () => new BackgroundJobClient(_storage.Object, null, _stateChanger.Object)); + () => new BackgroundJobClient(_storage.Object, _clock.Object, null, _stateChanger.Object)); Assert.Equal("factory", exception.ParamName); } @@ -59,7 +71,7 @@ public void Ctor_ThrowsAnException_WhenFactoryIsNull() public void Ctor_ThrowsAnException_WhenStateChangerIsNull() { var exception = Assert.Throws( - () => new BackgroundJobClient(_storage.Object, _factory.Object, null)); + () => new BackgroundJobClient(_storage.Object, _clock.Object, _factory.Object, null)); Assert.Equal("stateChanger", exception.ParamName); } @@ -195,7 +207,7 @@ public static void Method() private BackgroundJobClient CreateClient() { - return new BackgroundJobClient(_storage.Object, _factory.Object, _stateChanger.Object); + return new BackgroundJobClient(_storage.Object, _clock.Object, _factory.Object, _stateChanger.Object); } } } diff --git a/tests/Hangfire.Core.Tests/Client/BackgroundJobFactoryFacts.cs b/tests/Hangfire.Core.Tests/Client/BackgroundJobFactoryFacts.cs index 9baee9a90..7d05bfde7 100644 --- a/tests/Hangfire.Core.Tests/Client/BackgroundJobFactoryFacts.cs +++ b/tests/Hangfire.Core.Tests/Client/BackgroundJobFactoryFacts.cs @@ -25,20 +25,21 @@ public class BackgroundJobFactoryFacts public BackgroundJobFactoryFacts() { var storage = new Mock(); + var clock = new Mock(); var connection = new Mock(); var state = new Mock(); _backgroundJob = new BackgroundJobMock(); - _context = new Mock(storage.Object, connection.Object, _backgroundJob.Job, state.Object) + _context = new Mock(storage.Object, clock.Object, connection.Object, _backgroundJob.Job, state.Object) { CallBase = true }; - + _filters = new List(); _filterProvider = new Mock(); _filterProvider.Setup(x => x.GetFilters(It.IsNotNull())).Returns( _filters.Select(f => new JobFilter(f, JobFilterScope.Type, null))); - + _innerFactory = new Mock(); _innerFactory.Setup(x => x.Create(_context.Object)).Returns(_backgroundJob.Object); } @@ -71,7 +72,7 @@ public void Run_ThrowsAnException_WhenContextIsNull() Assert.Equal("context", exception.ParamName); } - + [Fact] public void Run_CallsInnerFactory_ToCreateAJob() { @@ -80,7 +81,7 @@ public void Run_CallsInnerFactory_ToCreateAJob() factory.Create(_context.Object); _innerFactory.Verify( - x => x.Create(_context.Object), + x => x.Create(_context.Object), Times.Once); } @@ -120,7 +121,7 @@ public void Run_CallsExceptionFilter_OnException() // Act Assert.Throws( () => factory.Create(_context.Object)); - + // Assert filter.Verify(x => x.OnClientException( It.IsNotNull())); @@ -227,7 +228,7 @@ public void Run_DoesNotCallBoth_CreateJob_And_OnCreated_WhenFilterCancelsThis_An filter.Setup(x => x.OnCreating(It.IsAny())) .Callback((CreatingContext x) => x.Canceled = true); - + var factory = CreateFactory(); // Act @@ -237,7 +238,7 @@ public void Run_DoesNotCallBoth_CreateJob_And_OnCreated_WhenFilterCancelsThis_An Assert.Null(jobId); _innerFactory.Verify( - x => x.Create(It.IsAny()), + x => x.Create(It.IsAny()), Times.Never); filter.Verify(x => x.OnCreated(It.IsAny()), Times.Never); @@ -283,7 +284,7 @@ public void Run_DoesNotCall_CreateJob_And_OnCreated_WhenExceptionOccured_DuringC // Assert _innerFactory.Verify( - x => x.Create(It.IsAny()), + x => x.Create(It.IsAny()), Times.Never); filter.Verify(x => x.OnCreated(It.IsAny()), Times.Never); diff --git a/tests/Hangfire.Core.Tests/Client/ClientExceptionContextFacts.cs b/tests/Hangfire.Core.Tests/Client/ClientExceptionContextFacts.cs index 17cee1b19..39f54ee62 100644 --- a/tests/Hangfire.Core.Tests/Client/ClientExceptionContextFacts.cs +++ b/tests/Hangfire.Core.Tests/Client/ClientExceptionContextFacts.cs @@ -15,12 +15,13 @@ public class ClientExceptionContextFacts public ClientExceptionContextFacts() { var storage = new Mock(); + var clock = new Mock(); var connection = new Mock(); var job = Job.FromExpression(() => TestMethod()); var state = new Mock(); _createContext = new CreateContext( - storage.Object, connection.Object, job, state.Object); + storage.Object, clock.Object, connection.Object, job, state.Object); } [Fact] diff --git a/tests/Hangfire.Core.Tests/Client/CreateContextFacts.cs b/tests/Hangfire.Core.Tests/Client/CreateContextFacts.cs index 8f0575ed8..198dd8e87 100644 --- a/tests/Hangfire.Core.Tests/Client/CreateContextFacts.cs +++ b/tests/Hangfire.Core.Tests/Client/CreateContextFacts.cs @@ -17,6 +17,7 @@ public class CreateContextFacts private readonly Mock _state; private readonly Mock _connection; private readonly Mock _storage; + private readonly Mock _clock; public CreateContextFacts() { @@ -24,22 +25,32 @@ public CreateContextFacts() _state = new Mock(); _connection = new Mock(); _storage = new Mock(); + _clock = new Mock(); } [Fact] public void Ctor_ThrowsAnException_WhenStorageIsNull() { var exception = Assert.Throws( - () => new CreateContext(null, _connection.Object, _job, _state.Object)); + () => new CreateContext(null, _clock.Object, _connection.Object, _job, _state.Object)); Assert.Equal("storage", exception.ParamName); } + [Fact] + public void Ctor_ThrowsAnException_WhenClockIsNull() + { + var exception = Assert.Throws( + () => new CreateContext(_storage.Object, null, _connection.Object, _job, _state.Object)); + + Assert.Equal("clock", exception.ParamName); + } + [Fact] public void Ctor_ThrowsAnException_WhenConnectionIsNull() { var exception = Assert.Throws( - () => new CreateContext(_storage.Object, null, _job, _state.Object)); + () => new CreateContext(_storage.Object, _clock.Object, null, _job, _state.Object)); Assert.Equal("connection", exception.ParamName); } @@ -48,7 +59,7 @@ public void Ctor_ThrowsAnException_WhenConnectionIsNull() public void Ctor_ThrowsAnException_WhenJobIsNull() { var exception = Assert.Throws( - () => new CreateContext(_storage.Object, _connection.Object, null, _state.Object)); + () => new CreateContext(_storage.Object, _clock.Object, _connection.Object, null, _state.Object)); Assert.Equal("job", exception.ParamName); } @@ -57,7 +68,7 @@ public void Ctor_ThrowsAnException_WhenJobIsNull() public void Ctor_DoesNotThrowAnException_WhenStateIsNull() { // Does not throw - new CreateContext(_storage.Object, _connection.Object, _job, null); + new CreateContext(_storage.Object, _clock.Object, _connection.Object, _job, null); } [Fact] @@ -66,6 +77,7 @@ public void Ctor_CorrectlyInitializes_AllProperties() var context = CreateContext(); Assert.Same(_storage.Object, context.Storage); + Assert.Same(_clock.Object, context.Clock); Assert.Same(_connection.Object, context.Connection); Assert.Same(_job, context.Job); Assert.Same(_state.Object, context.InitialState); @@ -90,7 +102,7 @@ public static void Method() private CreateContext CreateContext() { - return new CreateContext(_storage.Object, _connection.Object, _job, _state.Object); + return new CreateContext(_storage.Object, _clock.Object, _connection.Object, _job, _state.Object); } } } diff --git a/tests/Hangfire.Core.Tests/Client/CreatedContextFacts.cs b/tests/Hangfire.Core.Tests/Client/CreatedContextFacts.cs index 95ab83386..615f43f4e 100644 --- a/tests/Hangfire.Core.Tests/Client/CreatedContextFacts.cs +++ b/tests/Hangfire.Core.Tests/Client/CreatedContextFacts.cs @@ -62,16 +62,19 @@ public void SetJobParameter_ThrowsAnException_AfterCreateJobWasCalled() () => context.SetJobParameter("name", "value")); } - public static void TestMethod() { } + public static void TestMethod() + { + } private CreatedContext CreateContext() { var storage = new Mock(); + var clock = new Mock(); var connection = new Mock(); var job = Job.FromExpression(() => TestMethod()); var state = new Mock(); - - var createContext = new CreateContext(storage.Object, connection.Object, job, state.Object); + + var createContext = new CreateContext(storage.Object, clock.Object, connection.Object, job, state.Object); return new CreatedContext(createContext, _backgroundJob.Object, true, _exception); } } diff --git a/tests/Hangfire.Core.Tests/Client/CreatingContextFacts.cs b/tests/Hangfire.Core.Tests/Client/CreatingContextFacts.cs index 7c8a7cdcd..e99f716e2 100644 --- a/tests/Hangfire.Core.Tests/Client/CreatingContextFacts.cs +++ b/tests/Hangfire.Core.Tests/Client/CreatingContextFacts.cs @@ -113,17 +113,20 @@ public void GetJobParameter_ThrowsAnException_WhenParameterCouldNotBeDeserialize () => context.GetJobParameter("name")); } - public static void TestMethod() { } + public static void TestMethod() + { + } private CreatingContext CreateContext() { var storage = new Mock(); + var clock = new Mock(); var connection = new Mock(); var job = Job.FromExpression(() => TestMethod()); var state = new Mock(); - var createContext = new CreateContext(storage.Object, connection.Object, job, state.Object); - return new CreatingContext(createContext); + var createContext = new CreateContext(storage.Object, clock.Object, connection.Object, job, state.Object); + return new CreatingContext(createContext); } } } diff --git a/tests/Hangfire.Core.Tests/LatencyTimeoutAttributeFacts.cs b/tests/Hangfire.Core.Tests/LatencyTimeoutAttributeFacts.cs index dc20088c3..3851e6436 100644 --- a/tests/Hangfire.Core.Tests/LatencyTimeoutAttributeFacts.cs +++ b/tests/Hangfire.Core.Tests/LatencyTimeoutAttributeFacts.cs @@ -17,6 +17,8 @@ public LatencyTimeoutAttributeFacts() _context = new ElectStateContextMock(); _context.ApplyContext.BackgroundJob.Id = JobId; _context.ApplyContext.NewStateObject = state; + + _context.ApplyContext.Clock.SetupGet(x => x.UtcNow).Returns(DateTime.UtcNow); } [Fact] @@ -24,7 +26,7 @@ public void Ctor_ThrowsAnException_WhenTimeoutInSecondsValueIsNegative() { var exception = Assert.Throws( () => CreateFilter(-1)); - + Assert.Equal("timeoutInSeconds", exception.ParamName); } diff --git a/tests/Hangfire.Core.Tests/Mocks/ApplyStateContextMock.cs b/tests/Hangfire.Core.Tests/Mocks/ApplyStateContextMock.cs index 025156cb2..d9c1c7e40 100644 --- a/tests/Hangfire.Core.Tests/Mocks/ApplyStateContextMock.cs +++ b/tests/Hangfire.Core.Tests/Mocks/ApplyStateContextMock.cs @@ -12,6 +12,7 @@ class ApplyStateContextMock public ApplyStateContextMock() { Storage = new Mock(); + Clock = new Mock(); Connection = new Mock(); Transaction = new Mock(); BackgroundJob = new BackgroundJobMock(); @@ -22,6 +23,7 @@ public ApplyStateContextMock() _context = new Lazy( () => new ApplyStateContext( Storage.Object, + Clock.Object, Connection.Object, Transaction.Object, BackgroundJob.Object, @@ -32,14 +34,15 @@ public ApplyStateContextMock() }); } + public Mock Clock { get; set; } public Mock Storage { get; set; } - public Mock Connection { get; set; } - public Mock Transaction { get; set; } - public BackgroundJobMock BackgroundJob { get; set; } + public Mock Connection { get; set; } + public Mock Transaction { get; set; } + public BackgroundJobMock BackgroundJob { get; set; } public IState NewStateObject { get; set; } public Mock NewState { get; set; } public string OldStateName { get; set; } - public TimeSpan JobExpirationTimeout { get; set; } + public TimeSpan JobExpirationTimeout { get; set; } public ApplyStateContext Object => _context.Value; } diff --git a/tests/Hangfire.Core.Tests/Mocks/BackgroundProcessContextMock.cs b/tests/Hangfire.Core.Tests/Mocks/BackgroundProcessContextMock.cs index 2300efe78..e9934004a 100644 --- a/tests/Hangfire.Core.Tests/Mocks/BackgroundProcessContextMock.cs +++ b/tests/Hangfire.Core.Tests/Mocks/BackgroundProcessContextMock.cs @@ -14,6 +14,7 @@ public BackgroundProcessContextMock() { ServerId = "server"; Storage = new Mock(); + Clock = new Mock(); Properties = new Dictionary(); ExecutionId = Guid.NewGuid(); StoppingTokenSource = new CancellationTokenSource(); @@ -21,15 +22,17 @@ public BackgroundProcessContextMock() ShutdownTokenSource = new CancellationTokenSource(); _context = new Lazy( - () => new BackgroundProcessContext(ServerId, Storage.Object, Properties, ExecutionId, + () => new BackgroundProcessContext(ServerId, Storage.Object, Clock.Object, Properties, ExecutionId, StoppingTokenSource.Token, StoppedTokenSource.Token, ShutdownTokenSource.Token)); } + public Mock Clock { get; set; } + public BackgroundProcessContext Object => _context.Value; public string ServerId { get; set; } public Mock Storage { get; set; } - public IDictionary Properties { get; set; } + public IDictionary Properties { get; set; } public Guid ExecutionId { get; set; } public CancellationTokenSource StoppingTokenSource { get; set; } public CancellationTokenSource StoppedTokenSource { get; set; } diff --git a/tests/Hangfire.Core.Tests/Mocks/CreateContextMock.cs b/tests/Hangfire.Core.Tests/Mocks/CreateContextMock.cs index bbe2524f6..11b987512 100644 --- a/tests/Hangfire.Core.Tests/Mocks/CreateContextMock.cs +++ b/tests/Hangfire.Core.Tests/Mocks/CreateContextMock.cs @@ -14,6 +14,7 @@ class CreateContextMock public CreateContextMock() { Storage = new Mock(); + Clock = new Mock(); Connection = new Mock(); Job = Job.FromExpression(() => Method()); InitialState = new Mock(); @@ -21,15 +22,17 @@ public CreateContextMock() _context = new Lazy( () => new CreateContext( Storage.Object, + Clock.Object, Connection.Object, Job, InitialState.Object)); } public Mock Storage { get; set; } + public Mock Clock { get; set; } public Mock Connection { get; set; } public Job Job { get; set; } - public Mock InitialState { get; set; } + public Mock InitialState { get; set; } public CreateContext Object => _context.Value; diff --git a/tests/Hangfire.Core.Tests/Mocks/StateChangeContextMock.cs b/tests/Hangfire.Core.Tests/Mocks/StateChangeContextMock.cs index 0bc11f9be..54a4c6bf9 100644 --- a/tests/Hangfire.Core.Tests/Mocks/StateChangeContextMock.cs +++ b/tests/Hangfire.Core.Tests/Mocks/StateChangeContextMock.cs @@ -14,6 +14,7 @@ class StateChangeContextMock public StateChangeContextMock() { Storage = new Mock(); + Clock = new Mock(); Connection = new Mock(); BackgroundJobId = "JobId"; NewState = new Mock(); @@ -23,6 +24,7 @@ public StateChangeContextMock() _context = new Lazy( () => new StateChangeContext( Storage.Object, + Clock.Object, Connection.Object, BackgroundJobId, NewState.Object, @@ -31,12 +33,13 @@ public StateChangeContextMock() } public Mock Storage { get; set; } + public Mock Clock { get; set; } public Mock Connection { get; set; } public string BackgroundJobId { get; set; } public Mock NewState { get; set; } - public IEnumerable ExpectedStates { get; set; } + public IEnumerable ExpectedStates { get; set; } public CancellationToken CancellationToken { get; set; } - + public StateChangeContext Object => _context.Value; } } diff --git a/tests/Hangfire.Core.Tests/PreserveCultureAttributeFacts.cs b/tests/Hangfire.Core.Tests/PreserveCultureAttributeFacts.cs index e4394f716..a0c23038b 100644 --- a/tests/Hangfire.Core.Tests/PreserveCultureAttributeFacts.cs +++ b/tests/Hangfire.Core.Tests/PreserveCultureAttributeFacts.cs @@ -22,11 +22,12 @@ public PreserveCultureAttributeFacts() _connection = new Mock(); var storage = new Mock(); + var clock = new Mock(); var backgroundJob = new BackgroundJobMock { Id = JobId }; var state = new Mock(); var createContext = new CreateContext( - storage.Object, _connection.Object, backgroundJob.Job, state.Object); + storage.Object, clock.Object, _connection.Object, backgroundJob.Job, state.Object); _creatingContext = new CreatingContext(createContext); var performContext = new PerformContext( @@ -85,8 +86,8 @@ public void OnPerforming_SetsThreadCultures_ToTheSpecifiedOnesInJobParameters() [Fact] public void OnPerforming_DoesNotDoAnything_WhenCultureJobParameterIsNotSet() { - _connection.Setup(x => x.GetJobParameter(JobId, "CurrentCulture")).Returns((string)null); - _connection.Setup(x => x.GetJobParameter(JobId, "CurrentUICulture")).Returns((string)null); + _connection.Setup(x => x.GetJobParameter(JobId, "CurrentCulture")).Returns((string) null); + _connection.Setup(x => x.GetJobParameter(JobId, "CurrentUICulture")).Returns((string) null); CultureHelper.SetCurrentCulture("en-US"); CultureHelper.SetCurrentUICulture("en-US"); @@ -126,8 +127,8 @@ public void OnPerformed_RestoresPreviousCurrentCulture() [Fact] public void OnPerformed_RestoresPreviousCurrentCulture_OnlyIfItWasChanged() { - _connection.Setup(x => x.GetJobParameter(JobId, "CurrentCulture")).Returns((string)null); - _connection.Setup(x => x.GetJobParameter(JobId, "CurrentUICulture")).Returns((string)null); + _connection.Setup(x => x.GetJobParameter(JobId, "CurrentCulture")).Returns((string) null); + _connection.Setup(x => x.GetJobParameter(JobId, "CurrentUICulture")).Returns((string) null); CultureHelper.SetCurrentCulture("en-US"); CultureHelper.SetCurrentUICulture("en-US"); @@ -140,7 +141,9 @@ public void OnPerformed_RestoresPreviousCurrentCulture_OnlyIfItWasChanged() Assert.Equal("en-US", CultureInfo.CurrentUICulture.Name); } - public static void Sample() { } + public static void Sample() + { + } private CaptureCultureAttribute CreateFilter() { diff --git a/tests/Hangfire.Core.Tests/RecurringJobManagerFacts.cs b/tests/Hangfire.Core.Tests/RecurringJobManagerFacts.cs index 6314f39f2..d401137ab 100644 --- a/tests/Hangfire.Core.Tests/RecurringJobManagerFacts.cs +++ b/tests/Hangfire.Core.Tests/RecurringJobManagerFacts.cs @@ -6,6 +6,7 @@ using Hangfire.Storage; using Moq; using Xunit; + #pragma warning disable 618 // ReSharper disable AssignNullToNotNullAttribute @@ -23,9 +24,9 @@ public class RecurringJobManagerFacts private readonly Mock _factory; private readonly Mock _stateMachine; private readonly DateTime _now = new DateTime(2017, 03, 30, 15, 30, 0, DateTimeKind.Utc); - private readonly Func _nowFactory; private readonly BackgroundJob _backgroundJob; private readonly Mock _timeZoneResolver; + private readonly Mock _clock; public RecurringJobManagerFacts() { @@ -37,7 +38,9 @@ public RecurringJobManagerFacts() _factory = new Mock(); _stateMachine = new Mock(); _factory.SetupGet(x => x.StateMachine).Returns(_stateMachine.Object); - _nowFactory = () => _now; + + _clock = new Mock(); + _clock.SetupGet(x => x.UtcNow).Returns(_now); _timeZoneResolver = new Mock(); _timeZoneResolver.Setup(x => x.GetTimeZoneById(It.IsAny())).Returns(TimeZoneInfo.Utc); @@ -59,36 +62,36 @@ public RecurringJobManagerFacts() public void Ctor_ThrowsAnException_WhenStorageIsNull() { var exception = Assert.Throws( - () => new RecurringJobManager(null, _factory.Object)); + () => new RecurringJobManager(null, _clock.Object, _factory.Object)); Assert.Equal("storage", exception.ParamName); } [Fact] - public void Ctor_ThrowsAnException_WhenFactoryIsNull() + public void Ctor_ThrowsAnException_WhenClockIsNull() { var exception = Assert.Throws( - () => new RecurringJobManager(_storage.Object, (IBackgroundJobFactory)null)); + () => new RecurringJobManager(_storage.Object, null, _factory.Object)); - Assert.Equal("factory", exception.ParamName); + Assert.Equal("clock", exception.ParamName); } [Fact] - public void Ctor_ThrowsAnException_WhenTimeZoneResolverIsNull() + public void Ctor_ThrowsAnException_WhenFactoryIsNull() { var exception = Assert.Throws( - () => new RecurringJobManager(_storage.Object, _factory.Object, null, _nowFactory)); + () => new RecurringJobManager(_storage.Object, _clock.Object, (IBackgroundJobFactory) null)); - Assert.Equal("timeZoneResolver", exception.ParamName); + Assert.Equal("factory", exception.ParamName); } [Fact] - public void Ctor_ThrowsAnException_WhenNowFactoryIsNull() + public void Ctor_ThrowsAnException_WhenTimeZoneResolverIsNull() { var exception = Assert.Throws( - () => new RecurringJobManager(_storage.Object, _factory.Object, _timeZoneResolver.Object, null)); + () => new RecurringJobManager(_storage.Object, _clock.Object, _factory.Object, null)); - Assert.Equal("nowFactory", exception.ParamName); + Assert.Equal("timeZoneResolver", exception.ParamName); } [Fact] @@ -123,7 +126,7 @@ public void AddOrUpdate_ThrowsAnException_WhenQueueNameIsNull() Assert.Equal("queue", exception.ParamName); } - + [Fact] public void AddOrUpdate_ThrowsAnException_WhenCronExpressionIsNull() { @@ -209,7 +212,7 @@ public void AddOrUpdate_SetsTheRecurringJobEntry() _transaction.Verify(x => x.SetRangeInHash( $"recurring-job:{_id}", - It.Is>(rj => + It.Is>(rj => rj["Cron"] == "* * * * *" && !String.IsNullOrEmpty(rj["Job"]) && JobHelper.DeserializeDateTime(rj["CreatedAt"]) > _now.AddMinutes(-1)))); @@ -277,7 +280,7 @@ public void AddOrUpdate_EnsuresExistingOldJobsAreUpdated() // Assert _transaction.Verify(x => x.SetRangeInHash( - $"recurring-job:{_id}", + $"recurring-job:{_id}", It.Is>(dict => dict.Count == 1 && dict["V"] == "2"))); _transaction.Verify(x => x.AddToSet("recurring-jobs", _id, JobHelper.ToTimestamp(_now))); @@ -295,8 +298,8 @@ public void AddOrUpdate_CanAddRecurringJob_WithCronThatNeverFires() // Assert _transaction.Verify(x => x.SetRangeInHash( - $"recurring-job:{_id}", - It.Is>(dict => + $"recurring-job:{_id}", + It.Is>(dict => dict.ContainsKey("Cron") && dict["Cron"] == "0 0 31 2 *" && !dict.ContainsKey("NextExecution")))); @@ -414,7 +417,7 @@ public void Trigger_EnqueuedJobToTheSpecificQueue_IfSpecified() // Assert _stateMachine.Verify(x => x.ApplyState(It.Is(context => - ((EnqueuedState)context.NewState).Queue == "my_queue"))); + ((EnqueuedState) context.NewState).Queue == "my_queue"))); } [Fact] @@ -520,9 +523,11 @@ public void HandlesChangingProcessOfInvocationDataSerialization() private RecurringJobManager CreateManager() { - return new RecurringJobManager(_storage.Object, _factory.Object, _timeZoneResolver.Object, _nowFactory); + return new RecurringJobManager(_storage.Object, _clock.Object, _factory.Object, _timeZoneResolver.Object); } - public static void Method() { } + public static void Method() + { + } } } diff --git a/tests/Hangfire.Core.Tests/Server/BackgroundProcessContextFacts.cs b/tests/Hangfire.Core.Tests/Server/BackgroundProcessContextFacts.cs index 85c24fe96..a9b5d99fa 100644 --- a/tests/Hangfire.Core.Tests/Server/BackgroundProcessContextFacts.cs +++ b/tests/Hangfire.Core.Tests/Server/BackgroundProcessContextFacts.cs @@ -5,6 +5,7 @@ using Hangfire.Server; using Moq; using Xunit; + #pragma warning disable 618 // ReSharper disable AssignNullToNotNullAttribute @@ -17,11 +18,13 @@ public class BackgroundProcessContextFacts private readonly Mock _storage; private readonly CancellationTokenSource _cts; private readonly Dictionary _properties; + private readonly Mock _clock; public BackgroundProcessContextFacts() { _storage = new Mock(); - _properties = new Dictionary {{"key", "value"}}; + _clock = new Mock(); + _properties = new Dictionary { { "key", "value" } }; _cts = new CancellationTokenSource(); } @@ -29,7 +32,7 @@ public BackgroundProcessContextFacts() public void Ctor_ThrowsAnException_WhenServerIdIsNull() { var exception = Assert.Throws( - () => new BackgroundProcessContext(null, _storage.Object, _properties, _cts.Token)); + () => new BackgroundProcessContext(null, _storage.Object, _clock.Object, _properties, _cts.Token)); Assert.Equal("serverId", exception.ParamName); } @@ -38,16 +41,25 @@ public void Ctor_ThrowsAnException_WhenServerIdIsNull() public void Ctor_ThrowsAnException_WhenStorageIsNull() { var exception = Assert.Throws( - () => new BackgroundProcessContext(_serverId, null, _properties, _cts.Token)); + () => new BackgroundProcessContext(_serverId, null, _clock.Object, _properties, _cts.Token)); Assert.Equal("storage", exception.ParamName); } + [Fact] + public void Ctor_ThrowsAnException_WhenClockIsNull() + { + var exception = Assert.Throws( + () => new BackgroundProcessContext(_serverId, _storage.Object, null, _properties, _cts.Token)); + + Assert.Equal("clock", exception.ParamName); + } + [Fact] public void Ctor_ThrowsAnException_WhenPropertiesArgumentIsNull() { var exception = Assert.Throws( - () => new BackgroundProcessContext(_serverId, _storage.Object, null, _cts.Token)); + () => new BackgroundProcessContext(_serverId, _storage.Object, _clock.Object, null, _cts.Token)); Assert.Equal("properties", exception.ParamName); } @@ -55,7 +67,7 @@ public void Ctor_ThrowsAnException_WhenPropertiesArgumentIsNull() [Fact] public void Ctor_CorrectlyInitializes_AllTheProperties() { - var context = new BackgroundProcessContext(_serverId, _storage.Object, _properties, _cts.Token); + var context = new BackgroundProcessContext(_serverId, _storage.Object, _clock.Object, _properties, _cts.Token); Assert.Equal(_serverId, context.ServerId); Assert.True(_properties.SequenceEqual(context.Properties)); diff --git a/tests/Hangfire.Core.Tests/Server/BackgroundProcessingServerFacts.cs b/tests/Hangfire.Core.Tests/Server/BackgroundProcessingServerFacts.cs index 1e4e9d50e..242fd279d 100644 --- a/tests/Hangfire.Core.Tests/Server/BackgroundProcessingServerFacts.cs +++ b/tests/Hangfire.Core.Tests/Server/BackgroundProcessingServerFacts.cs @@ -17,6 +17,7 @@ public class BackgroundProcessingServerFacts private readonly List _processes; private readonly Dictionary _properties; private readonly Mock _connection; + private readonly Mock _clock; public BackgroundProcessingServerFacts() { @@ -26,22 +27,34 @@ public BackgroundProcessingServerFacts() _connection = new Mock(); _storage.Setup(x => x.GetConnection()).Returns(_connection.Object); + + _clock = new Mock(); } [Fact] public void Ctor_ThrowsAnException_WhenStorageIsNull() { var exception = Assert.Throws( - () => new BackgroundProcessingServer(null, _processes, _properties)); + () => new BackgroundProcessingServer(null, _clock.Object, _processes, _properties)); Assert.Equal("storage", exception.ParamName); } + + [Fact] + public void Ctor_ThrowsAnException_WhenClockIsNull() + { + var exception = Assert.Throws( + () => new BackgroundProcessingServer(_storage.Object, null, _processes, _properties)); + + Assert.Equal("clock", exception.ParamName); + } + [Fact] public void Ctor_ThrowsAnException_WhenProcessesArgumentIsNull() { var exception = Assert.Throws( - () => new BackgroundProcessingServer(_storage.Object, null, _properties)); + () => new BackgroundProcessingServer(_storage.Object, _clock.Object, null, _properties)); Assert.Equal("processes", exception.ParamName); } @@ -50,15 +63,18 @@ public void Ctor_ThrowsAnException_WhenProcessesArgumentIsNull() public void Ctor_ThrowsAnException_WhenPropertiesArgumentIsNull() { var exception = Assert.Throws( - () => new BackgroundProcessingServer(_storage.Object, _processes, null)); - + () => new BackgroundProcessingServer(_storage.Object, _clock.Object, _processes, null)); + Assert.Equal("properties", exception.ParamName); } [Fact] public void Ctor_AnnouncesTheServer_AndRemovesIt() { - using (CreateServer()) { Thread.Sleep(50); } + using (CreateServer()) + { + Thread.Sleep(50); + } _connection.Verify(x => x.AnnounceServer( It.IsNotNull(), @@ -75,16 +91,10 @@ public void Execute_StartsAllTheProcesses_InLoop_AndWaitsForThem() var component2Countdown = new CountdownEvent(5); var component1 = CreateProcessMock(); - component1.Setup(x => x.Execute(It.IsAny())).Callback(() => - { - component1Countdown.Signal(); - }); + component1.Setup(x => x.Execute(It.IsAny())).Callback(() => { component1Countdown.Signal(); }); var component2 = CreateProcessMock(); - component2.Setup(x => x.Execute(It.IsAny())).Callback(() => - { - component2Countdown.Signal(); - }); + component2.Setup(x => x.Execute(It.IsAny())).Callback(() => { component2Countdown.Signal(); }); // Act using (CreateServer()) @@ -99,7 +109,7 @@ public void Execute_StartsAllTheProcesses_InLoop_AndWaitsForThem() private BackgroundProcessingServer CreateServer() { - return new BackgroundProcessingServer(_storage.Object, _processes, _properties); + return new BackgroundProcessingServer(_storage.Object, _clock.Object, _processes, _properties); } private Mock CreateProcessMock() diff --git a/tests/Hangfire.Core.Tests/Server/DelayedJobSchedulerFacts.cs b/tests/Hangfire.Core.Tests/Server/DelayedJobSchedulerFacts.cs index 10b66bb3f..3d5818cc5 100644 --- a/tests/Hangfire.Core.Tests/Server/DelayedJobSchedulerFacts.cs +++ b/tests/Hangfire.Core.Tests/Server/DelayedJobSchedulerFacts.cs @@ -28,6 +28,8 @@ public DelayedJobSchedulerFacts() _connection = new Mock(); _context.Storage.Setup(x => x.GetConnection()).Returns(_connection.Object); + _context.Clock.SetupGet(x => x.UtcNow).Returns(DateTime.UtcNow); + _stateChanger = new Mock(); _transaction = new Mock(); _connection.Setup(x => x.CreateWriteTransaction()).Returns(_transaction.Object); diff --git a/tests/Hangfire.Core.Tests/Server/RecurringJobSchedulerFacts.cs b/tests/Hangfire.Core.Tests/Server/RecurringJobSchedulerFacts.cs index ea6a457d7..cc08422e4 100644 --- a/tests/Hangfire.Core.Tests/Server/RecurringJobSchedulerFacts.cs +++ b/tests/Hangfire.Core.Tests/Server/RecurringJobSchedulerFacts.cs @@ -21,7 +21,6 @@ public class RecurringJobSchedulerFacts private readonly Mock _connection; private readonly Mock _transaction; private readonly Dictionary _recurringJob; - private readonly Func _nowInstantFactory; private readonly Mock _timeZoneResolver; private readonly BackgroundProcessContextMock _context; private readonly Mock _factory; @@ -44,8 +43,6 @@ public RecurringJobSchedulerFacts() var timeZone = TimeZoneInfo.Local; - _nowInstantFactory = () => _nowInstant; - _timeZoneResolver = new Mock(); _timeZoneResolver.Setup(x => x.GetTimeZoneById(It.IsAny())).Throws(); _timeZoneResolver.Setup(x => x.GetTimeZoneById(timeZone.Id)).Returns(timeZone); @@ -53,6 +50,8 @@ public RecurringJobSchedulerFacts() // ReSharper disable once PossibleInvalidOperationException _nextInstant = _cronExpression.GetNextOccurrence(_nowInstant, timeZone).Value; + + _context.Clock.SetupGet(x => x.UtcNow).Returns(_nowInstant); _recurringJob = new Dictionary { { "Cron", _expressionString }, @@ -87,7 +86,7 @@ public RecurringJobSchedulerFacts() _factory = new Mock(); _factory.Setup(x => x.Create(It.IsAny())).Returns(_backgroundJobMock.Object); - + _stateMachine = new Mock(); _factory.SetupGet(x => x.StateMachine).Returns(_stateMachine.Object); } @@ -97,7 +96,7 @@ public void Ctor_ThrowsAnException_WhenJobFactoryIsNull() { var exception = Assert.Throws( // ReSharper disable once AssignNullToNotNullAttribute - () => new RecurringJobScheduler(null, _delay, _timeZoneResolver.Object, _nowInstantFactory)); + () => new RecurringJobScheduler(null, _delay, _timeZoneResolver.Object)); Assert.Equal("factory", exception.ParamName); } @@ -107,21 +106,11 @@ public void Ctor_ThrowsAnException_WhenTimeZoneResolverIsNull() { var exception = Assert.Throws( // ReSharper disable once AssignNullToNotNullAttribute - () => new RecurringJobScheduler(_factory.Object, _delay, null, _nowInstantFactory)); + () => new RecurringJobScheduler(_factory.Object, _delay, null)); Assert.Equal("timeZoneResolver", exception.ParamName); } - [Fact] - public void Ctor_ThrowsAnException_WhenNowInstantFactoryIsNull() - { - var exception = Assert.Throws( - // ReSharper disable once AssignNullToNotNullAttribute - () => new RecurringJobScheduler(_factory.Object, _delay, _timeZoneResolver.Object, null)); - - Assert.Equal("nowFactory", exception.ParamName); - } - [Fact] public void Execute_ThrowsAnException_WhenContextIsNull() { @@ -209,7 +198,7 @@ public void Execute_UpdatesRecurringJobParameters_OnCompletion(bool useJobStorag It.Is>(rj => rj.ContainsKey("NextExecution") && rj["NextExecution"] == JobHelper.SerializeDateTime(_nextInstant)))); - + _transaction.Verify(x => x.Commit()); } @@ -235,7 +224,7 @@ public void Execute_DoesNotEnqueueRecurringJob_AndDoesNotUpdateIt_ButNextExecuti It.Is>(rj => rj.ContainsKey("NextExecution") && rj["NextExecution"] == JobHelper.SerializeDateTime(_nextInstant)))); - + _transaction.Verify(x => x.Commit()); } @@ -367,7 +356,7 @@ public void Execute_DoesNotFixCreatedAtField_IfItExists(bool useJobStorageConnec // Act scheduler.Execute(_context.Object); - + // Assert _connection.Verify( x => x.SetRangeInHash( @@ -395,7 +384,7 @@ public void Execute_FixedMissingCreatedAtField(bool useJobStorageConnection) $"recurring-job:{RecurringJobId}", It.Is>(rj => rj.ContainsKey("CreatedAt"))), Times.Once); - + _transaction.Verify(x => x.Commit()); } @@ -422,7 +411,7 @@ public void Execute_UsesNextExecutionTime_WhenBothLastExecutionAndCreatedAtAreNo It.Is>(rj => rj.ContainsKey("LastExecution") && rj["LastExecution"] == JobHelper.SerializeDateTime(_nowInstant)))); - + _transaction.Verify(x => x.Commit()); } @@ -469,7 +458,7 @@ public void Execute_DoesNotEnqueueRecurringJob_WhenItIsCorrectAndItWasNotTrigger // Act scheduler.Execute(_context.Object); - + // Assert _stateMachine.Verify(x => x.ApplyState(It.IsAny()), Times.Never); } @@ -511,12 +500,12 @@ public void Execute_SchedulesNextExecution_AfterCreatingAJob(bool useJobStorageC _transaction.Verify(x => x.SetRangeInHash( $"recurring-job:{RecurringJobId}", It.Is>(rj => - rj.ContainsKey("NextExecution") && + rj.ContainsKey("NextExecution") && rj["NextExecution"] == JobHelper.SerializeDateTime(_nowInstant.AddMinutes(1))))); _transaction.Verify(x => x.AddToSet( - "recurring-jobs", - "recurring-job-id", + "recurring-jobs", + "recurring-job-id", JobHelper.ToTimestamp(_nowInstant.AddMinutes(1)))); _transaction.Verify(x => x.Commit()); @@ -752,8 +741,7 @@ private RecurringJobScheduler CreateScheduler(DateTime? lastExecution = null) var scheduler = new RecurringJobScheduler( _factory.Object, _delay, - _timeZoneResolver.Object, - _nowInstantFactory); + _timeZoneResolver.Object); if (lastExecution.HasValue) { diff --git a/tests/Hangfire.Core.Tests/States/ApplyStateContextFacts.cs b/tests/Hangfire.Core.Tests/States/ApplyStateContextFacts.cs index 4bcf25366..354227913 100644 --- a/tests/Hangfire.Core.Tests/States/ApplyStateContextFacts.cs +++ b/tests/Hangfire.Core.Tests/States/ApplyStateContextFacts.cs @@ -18,10 +18,12 @@ public class ApplyStateContextFacts private readonly BackgroundJobMock _backgroundJob; private readonly Mock _transaction; private readonly Mock _connection; + private readonly Mock _clock; public ApplyStateContextFacts() { _storage = new Mock(); + _clock = new Mock(); _connection = new Mock(); _transaction = new Mock(); _backgroundJob = new BackgroundJobMock(); @@ -33,17 +35,27 @@ public ApplyStateContextFacts() public void Ctor_ThrowsAnException_WhenStorageIsNull() { var exception = Assert.Throws( - () => new ApplyStateContext(null, _connection.Object, _transaction.Object, _backgroundJob.Object, _newState.Object, OldState)); + () => new ApplyStateContext(null, _clock.Object, _connection.Object, _transaction.Object, _backgroundJob.Object, _newState.Object, OldState)); Assert.Equal("storage", exception.ParamName); } + + [Fact] + public void Ctor_ThrowsAnException_WhenClockIsNull() + { + var exception = Assert.Throws( + () => new ApplyStateContext(_storage.Object, null, _connection.Object, _transaction.Object, _backgroundJob.Object, _newState.Object, OldState)); + + Assert.Equal("clock", exception.ParamName); + } + [Fact] public void Ctor_ThrowsAnException_WhenConnectionIsNull() { var exception = Assert.Throws( () => - new ApplyStateContext(_storage.Object, null, _transaction.Object, _backgroundJob.Object, + new ApplyStateContext(_storage.Object, _clock.Object, null, _transaction.Object, _backgroundJob.Object, _newState.Object, OldState)); Assert.Equal("connection", exception.ParamName); @@ -54,7 +66,7 @@ public void Ctor_ThrowsAnException_WhenTransactionIsNull() { var exception = Assert.Throws( () => - new ApplyStateContext(_storage.Object, _connection.Object, null, _backgroundJob.Object, _newState.Object, OldState)); + new ApplyStateContext(_storage.Object, _clock.Object, _connection.Object, null, _backgroundJob.Object, _newState.Object, OldState)); Assert.Equal("transaction", exception.ParamName); } @@ -63,7 +75,7 @@ public void Ctor_ThrowsAnException_WhenTransactionIsNull() public void Ctor_ThrowsAnException_WhenBackgroundJobIsNull() { var exception = Assert.Throws( - () => new ApplyStateContext(_storage.Object, _connection.Object, _transaction.Object, null, _newState.Object, OldState)); + () => new ApplyStateContext(_storage.Object, _clock.Object, _connection.Object, _transaction.Object, null, _newState.Object, OldState)); Assert.Equal("backgroundJob", exception.ParamName); } @@ -72,7 +84,7 @@ public void Ctor_ThrowsAnException_WhenBackgroundJobIsNull() public void Ctor_ThrowsAnException_WhenNewStateIsNull() { var exception = Assert.Throws( - () => new ApplyStateContext(_storage.Object, _connection.Object, _transaction.Object, _backgroundJob.Object, null, OldState)); + () => new ApplyStateContext(_storage.Object, _clock.Object, _connection.Object, _transaction.Object, _backgroundJob.Object, null, OldState)); Assert.Equal("newState", exception.ParamName); } @@ -82,6 +94,7 @@ public void Ctor_ShouldSetPropertiesCorrectly() { var context = new ApplyStateContext( _storage.Object, + _clock.Object, _connection.Object, _transaction.Object, _backgroundJob.Object, diff --git a/tests/Hangfire.Core.Tests/StatisticsHistoryAttributeFacts.cs b/tests/Hangfire.Core.Tests/StatisticsHistoryAttributeFacts.cs index b58080da6..bed402f1d 100644 --- a/tests/Hangfire.Core.Tests/StatisticsHistoryAttributeFacts.cs +++ b/tests/Hangfire.Core.Tests/StatisticsHistoryAttributeFacts.cs @@ -31,6 +31,8 @@ public StatisticsHistoryAttributeFacts() Transaction = _transaction } }; + + _context.ApplyContext.Clock.SetupGet(x => x.UtcNow).Returns(DateTime.UtcNow); } [Fact] diff --git a/tests/Hangfire.Core.Tests/Stubs/DashboardContextStub.cs b/tests/Hangfire.Core.Tests/Stubs/DashboardContextStub.cs index 03f4296b5..9235cf921 100644 --- a/tests/Hangfire.Core.Tests/Stubs/DashboardContextStub.cs +++ b/tests/Hangfire.Core.Tests/Stubs/DashboardContextStub.cs @@ -4,7 +4,7 @@ namespace Hangfire.Core.Tests.Stubs { class DashboardContextStub : DashboardContext { - public DashboardContextStub(DashboardOptions options) : base(new JobStorageStub(), options) + public DashboardContextStub(DashboardOptions options) : base(new JobStorageStub(), new ClockStub(), options) { Response = new DashboardResponseStub(); } diff --git a/tests/Hangfire.Core.Tests/Stubs/JobStorageStub.cs b/tests/Hangfire.Core.Tests/Stubs/JobStorageStub.cs index 56458b87f..720e65442 100644 --- a/tests/Hangfire.Core.Tests/Stubs/JobStorageStub.cs +++ b/tests/Hangfire.Core.Tests/Stubs/JobStorageStub.cs @@ -15,4 +15,8 @@ public override IStorageConnection GetConnection() throw new NotImplementedException(); } } + class ClockStub : IClock + { + public DateTime UtcNow => throw new NotImplementedException(); + } }