diff --git a/Hangfire.sln.DotSettings b/Hangfire.sln.DotSettings index 1c7ded50a..b35ed2adb 100644 --- a/Hangfire.sln.DotSettings +++ b/Hangfire.sln.DotSettings @@ -11,4 +11,7 @@ True <data><IncludeFilters /><ExcludeFilters><Filter ModuleMask="Hangfire.Core.Tests" ModuleVersionMask="*" ClassMask="*" FunctionMask="*" IsEnabled="True" /><Filter ModuleMask="Hangfire.SqlServer.Tests" ModuleVersionMask="*" ClassMask="*" FunctionMask="*" IsEnabled="True" /></ExcludeFilters></data> <data /> - True \ No newline at end of file + True + True + True + diff --git a/src/Hangfire.AspNetCore/Hangfire.AspNetCore.csproj b/src/Hangfire.AspNetCore/Hangfire.AspNetCore.csproj index 3dd9d2bb8..90d45a269 100644 --- a/src/Hangfire.AspNetCore/Hangfire.AspNetCore.csproj +++ b/src/Hangfire.AspNetCore/Hangfire.AspNetCore.csproj @@ -1,6 +1,6 @@  - net451;net461;netstandard1.3;netstandard2.0;netcoreapp3.0 + net48;net451;net461;netstandard1.3;netstandard2.0;netcoreapp3.0 portable false true @@ -8,7 +8,7 @@ Hangfire - + full @@ -28,7 +28,7 @@ - + diff --git a/src/Hangfire.Core/Common/JobFilterAttribute.cs b/src/Hangfire.Core/Common/JobFilterAttribute.cs index 35b252fc6..d62770613 100644 --- a/src/Hangfire.Core/Common/JobFilterAttribute.cs +++ b/src/Hangfire.Core/Common/JobFilterAttribute.cs @@ -16,8 +16,10 @@ using System; using System.Collections.Concurrent; +using System.ComponentModel; using System.Linq; using System.Reflection; +using Newtonsoft.Json; namespace Hangfire.Common { @@ -30,8 +32,11 @@ public abstract class JobFilterAttribute : Attribute, IJobFilter private static readonly ConcurrentDictionary MultiuseAttributeCache = new ConcurrentDictionary(); private int _order = JobFilter.DefaultOrder; + [JsonIgnore] public bool AllowMultiple => AllowsMultiple(GetType()); + [JsonProperty(DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] + [DefaultValue(JobFilter.DefaultOrder)] public int Order { get { return _order; } @@ -45,6 +50,11 @@ public int Order } } +#if !NETSTANDARD1_3 + [JsonIgnore] + public override object TypeId => base.TypeId; +#endif + private static bool AllowsMultiple(Type attributeType) { return MultiuseAttributeCache.GetOrAdd( diff --git a/src/Hangfire.Core/Dashboard/Content/js/hangfire.js b/src/Hangfire.Core/Dashboard/Content/js/hangfire.js index b11222512..292b802a6 100644 --- a/src/Hangfire.Core/Dashboard/Content/js/hangfire.js +++ b/src/Hangfire.Core/Dashboard/Content/js/hangfire.js @@ -61,9 +61,10 @@ })(); hangfire.RealtimeGraph = (function() { - function RealtimeGraph(element, succeeded, failed, succeededStr, failedStr, pollInterval) { + function RealtimeGraph(element, succeeded, failed, deleted, succeededStr, failedStr, deletedStr, pollInterval) { this._succeeded = succeeded; this._failed = failed; + this._deleted = deleted; this._last = Date.now(); this._pollInterval = pollInterval; @@ -72,7 +73,8 @@ data: { datasets: [ { label: succeededStr, borderColor: '#62B35F', backgroundColor: '#6FCD6D' }, - { label: failedStr, borderColor: '#BB4847', backgroundColor: '#D55251' } + { label: failedStr, borderColor: '#BB4847', backgroundColor: '#D55251' }, + { label: deletedStr, borderColor: '#777777', backgroundColor: '#919191' } ] }, options: { @@ -98,20 +100,24 @@ RealtimeGraph.prototype.appendHistory = function (statistics) { var newSucceeded = parseInt(statistics["succeeded:count"].intValue); var newFailed = parseInt(statistics["failed:count"].intValue); + var newDeleted = parseInt(statistics["deleted:count"].intValue); var now = Date.now(); if (this._succeeded !== null && this._failed !== null && (now - this._last < this._pollInterval * 2)) { var succeeded = Math.max(newSucceeded - this._succeeded, 0); var failed = Math.max(newFailed - this._failed, 0); + var deleted = Math.max(newDeleted - this._deleted, 0); - this._chart.data.datasets[0].data.push({ x: new Date(), y: succeeded }); - this._chart.data.datasets[1].data.push({ x: new Date(), y: failed }); + this._chart.data.datasets[0].data.push({ x: now, y: succeeded }); + this._chart.data.datasets[1].data.push({ x: now, y: failed }); + this._chart.data.datasets[2].data.push({ x: now, y: deleted }); this._chart.update(); } this._succeeded = newSucceeded; this._failed = newFailed; + this._deleted = newDeleted; this._last = now; }; @@ -232,10 +238,12 @@ if (realtimeElement) { var succeeded = parseInt($(realtimeElement).data('succeeded')); var failed = parseInt($(realtimeElement).data('failed')); + var deleted = parseInt($(realtimeElement).data('deleted')); var succeededStr = $(realtimeElement).data('succeeded-string'); var failedStr = $(realtimeElement).data('failed-string'); - var realtimeGraph = new Hangfire.RealtimeGraph(realtimeElement, succeeded, failed, succeededStr, failedStr, pollInterval); + var deletedStr = $(realtimeElement).data('deleted-string'); + var realtimeGraph = new Hangfire.RealtimeGraph(realtimeElement, succeeded, failed, deleted, succeededStr, failedStr, deletedStr, pollInterval); this._poller.addListener(function (data) { realtimeGraph.appendHistory(data); diff --git a/src/Hangfire.Core/Dashboard/Content/resx/Strings.Designer.cs b/src/Hangfire.Core/Dashboard/Content/resx/Strings.Designer.cs index 816480a33..aa4bd2a1a 100644 --- a/src/Hangfire.Core/Dashboard/Content/resx/Strings.Designer.cs +++ b/src/Hangfire.Core/Dashboard/Content/resx/Strings.Designer.cs @@ -8,8 +8,6 @@ // //------------------------------------------------------------------------------ -using System.Reflection; - namespace Hangfire.Dashboard.Resources { using System; @@ -41,7 +39,7 @@ internal Strings() { public static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Hangfire.Dashboard.Content.resx.Strings", typeof(Strings).GetTypeInfo().Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Hangfire.Dashboard.Content.resx.Strings", typeof(Strings).Assembly); resourceMan = temp; } return resourceMan; @@ -693,6 +691,15 @@ public static string JobsSidebarMenu_Succeeded { } } + /// + /// Looks up a localized string similar to Suspended. + /// + public static string JobsSidebarMenu_Suspended { + get { + return ResourceManager.GetString("JobsSidebarMenu_Suspended", resourceCulture); + } + } + /// /// Looks up a localized string similar to Back to site. /// @@ -837,6 +844,15 @@ public static string Metrics_SucceededJobs { } } + /// + /// Looks up a localized string similar to Suspended. + /// + public static string Metrics_SuspendedCount { + get { + return ResourceManager.GetString("Metrics_SuspendedCount", resourceCulture); + } + } + /// /// Looks up a localized string similar to Total Connections. /// @@ -1314,5 +1330,41 @@ public static string SucceededJobsPage_Title { return ResourceManager.GetString("SucceededJobsPage_Title", resourceCulture); } } + + /// + /// Looks up a localized string similar to All is OK – you have no suspended.. + /// + public static string SuspendedJobsPage_NoJobs { + get { + return ResourceManager.GetString("SuspendedJobsPage_NoJobs", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Suspended Jobs. + /// + public static string SuspendedJobsPage_Title { + get { + return ResourceManager.GetString("SuspendedJobsPage_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to <h4>This page can't be displayed</h4> + /// <p> + /// Don't worry, suspend is working as expected. Your current job storage does not support + /// some queries required to show this page. Please try to update your storage or wait until + /// the full command set is implemented. + /// </p> + /// <p> + /// Please go to the <a href="{0}">Scheduled jobs</a> page to see all the + /// scheduled jobs including retries. + /// </p>. + /// + public static string SuspendedPage_Warning_Html { + get { + return ResourceManager.GetString("SuspendedPage_Warning_Html", resourceCulture); + } + } } } diff --git a/src/Hangfire.Core/Dashboard/Content/resx/Strings.resx b/src/Hangfire.Core/Dashboard/Content/resx/Strings.resx index c9229800d..adb7d85ed 100644 --- a/src/Hangfire.Core/Dashboard/Content/resx/Strings.resx +++ b/src/Hangfire.Core/Dashboard/Content/resx/Strings.resx @@ -545,7 +545,33 @@ Error + + + Suspended + + + Suspended + + + <h4>This page can't be displayed</h4> + <p> + Don't worry, suspend is working as expected. Your current job storage does not support + some queries required to show this page. Please try to update your storage or wait until + the full command set is implemented. + </p> + <p> + Please go to the <a href="{0}">Scheduled jobs</a> page to see all the + scheduled jobs including retries. + </p> + + + All is OK – you have no suspended. + + + Suspended Jobs + Parameters + \ No newline at end of file diff --git a/src/Hangfire.Core/Dashboard/DashboardMetrics.cs b/src/Hangfire.Core/Dashboard/DashboardMetrics.cs index 5d5c29a5f..65cf7aff5 100644 --- a/src/Hangfire.Core/Dashboard/DashboardMetrics.cs +++ b/src/Hangfire.Core/Dashboard/DashboardMetrics.cs @@ -62,7 +62,7 @@ public static IEnumerable GetMetrics() } public static readonly DashboardMetric ServerCount = new DashboardMetric( - "servers:count", + "servers:count", "Metrics_Servers", page => new Metric(page.Statistics.Servers) { @@ -84,15 +84,23 @@ public static IEnumerable GetMetrics() page => { long retryCount; - using (var connection = page.Storage.GetConnection()) + + if (page.Statistics.Retries.HasValue) { - var storageConnection = connection as JobStorageConnection; - if (storageConnection == null) + retryCount = page.Statistics.Retries.Value; + } + else + { + using (var connection = page.Storage.GetConnection()) { - return null; - } + var storageConnection = connection as JobStorageConnection; + if (storageConnection == null) + { + return null; + } - retryCount = storageConnection.GetSetCount("retries"); + retryCount = storageConnection.GetSetCount("retries"); + } } return new Metric(retryCount) @@ -190,5 +198,26 @@ public static IEnumerable GetMetrics() Style = awaitingCount > 0 ? MetricStyle.Info : MetricStyle.Default }; }); + public static readonly DashboardMetric SuspendedCount = new DashboardMetric( + "suspended:count", + "Metrics_SuspendedCount", + page => + { + long suspendedCount = -1; + + using (var connection = page.Storage.GetConnection()) + { + var storageConnection = connection as JobStorageConnection; + if (storageConnection != null) + { + suspendedCount = storageConnection.GetSetCount("paused-jobs"); + } + } + + return new Metric(suspendedCount) + { + Style = suspendedCount > 0 ? MetricStyle.Info : MetricStyle.Default + }; + }); } } diff --git a/src/Hangfire.Core/Dashboard/DashboardRoutes.cs b/src/Hangfire.Core/Dashboard/DashboardRoutes.cs index f6ac2d9d6..038ee4784 100644 --- a/src/Hangfire.Core/Dashboard/DashboardRoutes.cs +++ b/src/Hangfire.Core/Dashboard/DashboardRoutes.cs @@ -176,6 +176,16 @@ static DashboardRoutes() Routes.AddRazorPage("/servers", x => new ServersPage()); Routes.AddRazorPage("/retries", x => new RetriesPage()); + Routes.AddRazorPage("/jobs/suspended", x => new SuspendedJobsPage()); + Routes.AddRecurringBatchCommand( + "/recurring/suspend", + (manager, jobId) => manager.SuspendJob(jobId)); + + Routes.AddRecurringBatchCommand( + "/recurring/resume", + (manager, jobId) => manager.ResumeJob(jobId)); + + #endregion } diff --git a/src/Hangfire.Core/Dashboard/JobsSidebarMenu.cs b/src/Hangfire.Core/Dashboard/JobsSidebarMenu.cs index 9a5ed3d92..331177abd 100644 --- a/src/Hangfire.Core/Dashboard/JobsSidebarMenu.cs +++ b/src/Hangfire.Core/Dashboard/JobsSidebarMenu.cs @@ -68,6 +68,12 @@ static JobsSidebarMenu() Active = page.RequestPath.StartsWith("/jobs/awaiting"), Metric = DashboardMetrics.AwaitingCount }); + Items.Add(page => new MenuItem(Strings.JobsSidebarMenu_Suspended, page.Url.To("/jobs/suspended")) + { + Active = page.RequestPath.StartsWith("/jobs/suspended"), + Metric = DashboardMetrics.SuspendedCount + }); + } } } \ No newline at end of file diff --git a/src/Hangfire.Core/Dashboard/Pages/HomePage.cshtml b/src/Hangfire.Core/Dashboard/Pages/HomePage.cshtml index d3eb918f5..1ab6f36c2 100644 --- a/src/Hangfire.Core/Dashboard/Pages/HomePage.cshtml +++ b/src/Hangfire.Core/Dashboard/Pages/HomePage.cshtml @@ -41,12 +41,14 @@ }

@Strings.HomePage_RealtimeGraph

- + data-failed-string="@Strings.HomePage_GraphHover_Failed" + data-deleted-string="@Strings.JobsSidebarMenu_Deleted">
+

diff --git a/src/Hangfire.Core/Dashboard/Pages/HomePage.cshtml.cs b/src/Hangfire.Core/Dashboard/Pages/HomePage.cshtml.cs index 9b94b8598..66e85a480 100644 --- a/src/Hangfire.Core/Dashboard/Pages/HomePage.cshtml.cs +++ b/src/Hangfire.Core/Dashboard/Pages/HomePage.cshtml.cs @@ -186,6 +186,16 @@ public override void Execute() Write(Statistics.Failed); + #line default + #line hidden +WriteLiteral("\" data-deleted=\""); + + + + #line 44 "..\..\Dashboard\Pages\HomePage.cshtml" + Write(Statistics.Deleted); + + #line default #line hidden WriteLiteral("\"\r\n data-succeeded-string=\""); @@ -206,12 +216,23 @@ public override void Execute() Write(Strings.HomePage_GraphHover_Failed); + #line default + #line hidden +WriteLiteral("\"\r\n data-deleted-string=\""); + + + + #line 47 "..\..\Dashboard\Pages\HomePage.cshtml" + Write(Strings.JobsSidebarMenu_Deleted); + + #line default #line hidden WriteLiteral(@""">
+

@@ -220,7 +241,7 @@ public override void Execute() - #line 54 "..\..\Dashboard\Pages\HomePage.cshtml" + #line 56 "..\..\Dashboard\Pages\HomePage.cshtml" Write("day".Equals(period, StringComparison.OrdinalIgnoreCase) ? "active" : null); @@ -230,7 +251,7 @@ public override void Execute() - #line 54 "..\..\Dashboard\Pages\HomePage.cshtml" + #line 56 "..\..\Dashboard\Pages\HomePage.cshtml" Write(Strings.Common_PeriodDay); @@ -240,7 +261,7 @@ public override void Execute() - #line 55 "..\..\Dashboard\Pages\HomePage.cshtml" + #line 57 "..\..\Dashboard\Pages\HomePage.cshtml" Write("week".Equals(period, StringComparison.OrdinalIgnoreCase) ? "active" : null); @@ -250,7 +271,7 @@ public override void Execute() - #line 55 "..\..\Dashboard\Pages\HomePage.cshtml" + #line 57 "..\..\Dashboard\Pages\HomePage.cshtml" Write(Strings.Common_PeriodWeek); @@ -260,7 +281,7 @@ public override void Execute() - #line 57 "..\..\Dashboard\Pages\HomePage.cshtml" + #line 59 "..\..\Dashboard\Pages\HomePage.cshtml" Write(Strings.HomePage_HistoryGraph); @@ -270,7 +291,7 @@ public override void Execute() - #line 60 "..\..\Dashboard\Pages\HomePage.cshtml" + #line 62 "..\..\Dashboard\Pages\HomePage.cshtml" if (succeeded != null && failed != null) { @@ -282,7 +303,7 @@ public override void Execute() - #line 63 "..\..\Dashboard\Pages\HomePage.cshtml" + #line 65 "..\..\Dashboard\Pages\HomePage.cshtml" Write(JsonConvert.SerializeObject(succeeded)); @@ -292,7 +313,7 @@ public override void Execute() - #line 64 "..\..\Dashboard\Pages\HomePage.cshtml" + #line 66 "..\..\Dashboard\Pages\HomePage.cshtml" Write(JsonConvert.SerializeObject(failed)); @@ -302,7 +323,7 @@ public override void Execute() - #line 65 "..\..\Dashboard\Pages\HomePage.cshtml" + #line 67 "..\..\Dashboard\Pages\HomePage.cshtml" Write(Strings.HomePage_GraphHover_Succeeded); @@ -312,7 +333,7 @@ public override void Execute() - #line 66 "..\..\Dashboard\Pages\HomePage.cshtml" + #line 68 "..\..\Dashboard\Pages\HomePage.cshtml" Write(Strings.HomePage_GraphHover_Failed); @@ -322,7 +343,7 @@ public override void Execute() - #line 67 "..\..\Dashboard\Pages\HomePage.cshtml" + #line 69 "..\..\Dashboard\Pages\HomePage.cshtml" Write(period); @@ -332,7 +353,7 @@ public override void Execute() - #line 69 "..\..\Dashboard\Pages\HomePage.cshtml" + #line 71 "..\..\Dashboard\Pages\HomePage.cshtml" } diff --git a/src/Hangfire.Core/Dashboard/Pages/SuspendedJobsPage.cshtml b/src/Hangfire.Core/Dashboard/Pages/SuspendedJobsPage.cshtml new file mode 100644 index 000000000..9d5633f91 --- /dev/null +++ b/src/Hangfire.Core/Dashboard/Pages/SuspendedJobsPage.cshtml @@ -0,0 +1,171 @@ +@* Generator: Template TypeVisibility: Internal GeneratePrettyNames: True *@ +@using System +@using System.Collections.Generic +@using Hangfire +@using Hangfire.Common +@using Hangfire.Dashboard +@using Hangfire.Dashboard.Pages +@using Hangfire.Dashboard.Resources +@using Hangfire.Storage +@inherits RazorPage +@{ + Layout = new LayoutPage(Strings.SuspendedJobsPage_Title); + + int from, perPage; + + int.TryParse(Query("from"), out from); + int.TryParse(Query("count"), out perPage); + + Pager pager = null; + List jobIds = null; + + using (var connection = Storage.GetConnection()) + { + var storageConnection = connection as JobStorageConnection; + + if (storageConnection != null) + { + pager = new Pager(@from, perPage, storageConnection.GetSetCount("paused-jobs")); + jobIds = storageConnection.GetRangeFromSet("retries", pager.FromRecord, pager.FromRecord + pager.RecordsPerPage - 1); + } + } +} + +@if (pager == null) +{ +
+ @Html.Raw(String.Format(Strings.SuspendedPage_Warning_Html, Url.To("/jobs/suspended"))) +
+} +else +{ +
+
+ @Html.JobsSidebar() +
+
+

@Strings.SuspendedJobsPage_Title

+ @if (jobIds.Count == 0) + { +
+ @Strings.SuspendedJobsPage_NoJobs +
+ } + else + { +
+
+ @if (!IsReadOnly) + { + + } + @if (!IsReadOnly) + { + + + @**@ + } + @Html.PerPageSelector(pager) +
+ +
+ + + + @if (!IsReadOnly) + { + + } + + + + + + + + + + @foreach (var jobId in jobIds) + { + JobData jobData; + StateData stateData; + string _jobId = jobId; + using (var connection = Storage.GetConnection()) + { + var recurringJob = connection.GetRecurringJob(jobId, new DefaultTimeZoneResolver(), DateTime.UtcNow); + if (recurringJob != null) + { + _jobId = recurringJob.LastJobId; + } + jobData = connection.GetJobData(_jobId); + stateData = connection.GetStateData(_jobId); + } + + + @if (!IsReadOnly) + { + + } + + @if (jobData == null) + { + + } + else + { + + + + + + } + + } + +
+ + @Strings.Common_Id@Strings.Common_State@Strings.Common_Job@Strings.Common_Reason@Strings.Common_Retry@Strings.Common_Created
+ + + @Html.JobIdLink(_jobId) + Job expired. + @Html.StateLabel(jobData.State) + + @Html.JobNameLink(_jobId, jobData.Job) + + @(stateData?.Reason) + + @if (stateData != null && stateData.Data.ContainsKey("EnqueueAt")) + { + @Html.RelativeTime(JobHelper.DeserializeDateTime(stateData.Data["EnqueueAt"])) + } + + @Html.RelativeTime(jobData.CreatedAt) +
+
+ + @Html.Paginator(pager) +
+ } +
+
+} \ No newline at end of file diff --git a/src/Hangfire.Core/Dashboard/Pages/SuspendedJobsPage.cshtml.cs b/src/Hangfire.Core/Dashboard/Pages/SuspendedJobsPage.cshtml.cs new file mode 100644 index 000000000..6d588f44f --- /dev/null +++ b/src/Hangfire.Core/Dashboard/Pages/SuspendedJobsPage.cshtml.cs @@ -0,0 +1,648 @@ +#pragma warning disable 1591 +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Hangfire.Dashboard.Pages +{ + + #line 2 "..\..\Dashboard\Pages\SuspendedJobsPage.cshtml" + using System; + + #line default + #line hidden + + #line 3 "..\..\Dashboard\Pages\SuspendedJobsPage.cshtml" + using System.Collections.Generic; + + #line default + #line hidden + using System.Linq; + using System.Text; + + #line 4 "..\..\Dashboard\Pages\SuspendedJobsPage.cshtml" + using Hangfire; + + #line default + #line hidden + + #line 5 "..\..\Dashboard\Pages\SuspendedJobsPage.cshtml" + using Hangfire.Common; + + #line default + #line hidden + + #line 6 "..\..\Dashboard\Pages\SuspendedJobsPage.cshtml" + using Hangfire.Dashboard; + + #line default + #line hidden + + #line 7 "..\..\Dashboard\Pages\SuspendedJobsPage.cshtml" + using Hangfire.Dashboard.Pages; + + #line default + #line hidden + + #line 8 "..\..\Dashboard\Pages\SuspendedJobsPage.cshtml" + using Hangfire.Dashboard.Resources; + + #line default + #line hidden + + #line 9 "..\..\Dashboard\Pages\SuspendedJobsPage.cshtml" + using Hangfire.Storage; + + #line default + #line hidden + + [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorGenerator", "2.0.0.0")] + internal partial class SuspendedJobsPage : RazorPage + { +#line hidden + + public override void Execute() + { + + +WriteLiteral("\r\n"); + + + + + + + + + + + + + #line 11 "..\..\Dashboard\Pages\SuspendedJobsPage.cshtml" + + Layout = new LayoutPage("Suspended Jobs"); + + int from, perPage; + + int.TryParse(Query("from"), out from); + int.TryParse(Query("count"), out perPage); + + Pager pager = null; + List jobIds = null; + + using (var connection = Storage.GetConnection()) + { + var storageConnection = connection as JobStorageConnection; + + if (storageConnection != null) + { + pager = new Pager(@from, perPage, storageConnection.GetSetCount("paused-jobs")); + jobIds = storageConnection.GetRangeFromSet("paused-jobs", pager.FromRecord, pager.FromRecord + pager.RecordsPerPage - 1); + } + } + + + + #line default + #line hidden +WriteLiteral("\r\n"); + + + + #line 34 "..\..\Dashboard\Pages\SuspendedJobsPage.cshtml" + if (pager == null) +{ + + + #line default + #line hidden +WriteLiteral("
\r\n "); + + + + #line 37 "..\..\Dashboard\Pages\SuspendedJobsPage.cshtml" + Write(Html.Raw(String.Format(Strings.RetriesPage_Warning_Html, Url.To("/jobs/scheduled")))); + + + #line default + #line hidden +WriteLiteral("\r\n
\r\n"); + + + + #line 39 "..\..\Dashboard\Pages\SuspendedJobsPage.cshtml" +} +else +{ + + + #line default + #line hidden +WriteLiteral("
\r\n
\r\n

"); + + + + #line 44 "..\..\Dashboard\Pages\SuspendedJobsPage.cshtml" + Write("Suspended Jobs"); + + + #line default + #line hidden +WriteLiteral("

\r\n"); + + + + #line 45 "..\..\Dashboard\Pages\SuspendedJobsPage.cshtml" + if (jobIds.Count == 0) + { + + + #line default + #line hidden +WriteLiteral("
\r\n "); + + + + #line 48 "..\..\Dashboard\Pages\SuspendedJobsPage.cshtml" + Write("No Suspended Jobs"); + + + #line default + #line hidden +WriteLiteral("\r\n
\r\n"); + + + + #line 50 "..\..\Dashboard\Pages\SuspendedJobsPage.cshtml" + } + else + { + + + #line default + #line hidden +WriteLiteral("
\r\n
\r\n"); + + + + #line 55 "..\..\Dashboard\Pages\SuspendedJobsPage.cshtml" + if (!IsReadOnly) + { + + + #line default + #line hidden +WriteLiteral(" \r\n"); + + + + #line 64 "..\..\Dashboard\Pages\SuspendedJobsPage.cshtml" + } + + + #line default + #line hidden + + + #line 65 "..\..\Dashboard\Pages\SuspendedJobsPage.cshtml" + if (!IsReadOnly) + { + + + #line default + #line hidden +WriteLiteral(" \r\n"); + + + + #line 75 "..\..\Dashboard\Pages\SuspendedJobsPage.cshtml" + } + + + #line default + #line hidden +WriteLiteral(" "); + + + + #line 76 "..\..\Dashboard\Pages\SuspendedJobsPage.cshtml" + Write(Html.PerPageSelector(pager)); + + + #line default + #line hidden +WriteLiteral("\r\n
\r\n\r\n
\r\n \r\n " + +" \r\n \r\n"); + + + + #line 83 "..\..\Dashboard\Pages\SuspendedJobsPage.cshtml" + if (!IsReadOnly) + { + + + #line default + #line hidden +WriteLiteral(" \r\n"); + + + + #line 88 "..\..\Dashboard\Pages\SuspendedJobsPage.cshtml" + } + + + #line default + #line hidden +WriteLiteral(" \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n"); + + + + #line 98 "..\..\Dashboard\Pages\SuspendedJobsPage.cshtml" + foreach (var jobId in jobIds) + { + JobData jobData; + StateData stateData; + string _jobId = jobId; + using (var connection = Storage.GetConnection()) + { + var recurringJob = connection.GetRecurringJob(jobId, new DefaultTimeZoneResolver(), DateTime.UtcNow); + + if (recurringJob != null) + { + _jobId = recurringJob.LastJobId; + } + jobData = connection.GetJobData(_jobId); + stateData = connection.GetStateData(_jobId); + } + + + + #line default + #line hidden +WriteLiteral(" \r\n"); + + + + #line 110 "..\..\Dashboard\Pages\SuspendedJobsPage.cshtml" + if (!IsReadOnly) + { + + + #line default + #line hidden +WriteLiteral(" \r\n"); + + + + #line 115 "..\..\Dashboard\Pages\SuspendedJobsPage.cshtml" + } + + + #line default + #line hidden +WriteLiteral(" \r\n"); + + + + #line 119 "..\..\Dashboard\Pages\SuspendedJobsPage.cshtml" + if (jobData == null) + { + + + #line default + #line hidden +WriteLiteral(" \r\n"); + + + + #line 122 "..\..\Dashboard\Pages\SuspendedJobsPage.cshtml" + } + else + { + + + #line default + #line hidden +WriteLiteral(" \r\n"); + + + +WriteLiteral(" \r\n"); + + + +WriteLiteral(" \r\n"); + + + +WriteLiteral(" \r\n"); + + + +WriteLiteral(" \r\n"); + + + + #line 143 "..\..\Dashboard\Pages\SuspendedJobsPage.cshtml" + } + + + #line default + #line hidden +WriteLiteral(" \r\n"); + + + + #line 145 "..\..\Dashboard\Pages\SuspendedJobsPage.cshtml" + } + + + #line default + #line hidden +WriteLiteral(" \r\n
\r\n " + +" \r\n "); + + + + #line 89 "..\..\Dashboard\Pages\SuspendedJobsPage.cshtml" + Write(Strings.Common_Id); + + + #line default + #line hidden +WriteLiteral(""); + + + + #line 90 "..\..\Dashboard\Pages\SuspendedJobsPage.cshtml" + Write(Strings.Common_State); + + + #line default + #line hidden +WriteLiteral(""); + + + + #line 91 "..\..\Dashboard\Pages\SuspendedJobsPage.cshtml" + Write(Strings.Common_Job); + + + #line default + #line hidden +WriteLiteral(""); + + + + #line 92 "..\..\Dashboard\Pages\SuspendedJobsPage.cshtml" + Write(Strings.Common_Reason); + + + #line default + #line hidden +WriteLiteral(""); + + + + #line 93 "..\..\Dashboard\Pages\SuspendedJobsPage.cshtml" + Write(Strings.Common_Retry); + + + #line default + #line hidden +WriteLiteral(""); + + + + #line 94 "..\..\Dashboard\Pages\SuspendedJobsPage.cshtml" + Write(Strings.Common_Created); + + + #line default + #line hidden +WriteLiteral("
\r\n " + +"
\r\n\r\n "); + + + + #line 150 "..\..\Dashboard\Pages\SuspendedJobsPage.cshtml" + Write(Html.Paginator(pager)); + + + #line default + #line hidden +WriteLiteral("\r\n
\r\n"); + + + + #line 152 "..\..\Dashboard\Pages\SuspendedJobsPage.cshtml" + } + + + #line default + #line hidden +WriteLiteral("
\r\n
\r\n"); + + + + #line 155 "..\..\Dashboard\Pages\SuspendedJobsPage.cshtml" +} + + #line default + #line hidden + + } + } +} +#pragma warning restore 1591 diff --git a/src/Hangfire.Core/Hangfire.Core.csproj b/src/Hangfire.Core/Hangfire.Core.csproj index 1bcb0b1d8..cf85ba8d0 100644 --- a/src/Hangfire.Core/Hangfire.Core.csproj +++ b/src/Hangfire.Core/Hangfire.Core.csproj @@ -1,6 +1,6 @@  - net45;net46;netstandard1.3;netstandard2.0; + net48;net46;net45;netstandard1.3;netstandard2.0; portable false true @@ -8,7 +8,7 @@ Hangfire - + full $(DefineConstants);FEATURE_CRONDESCRIPTOR;FEATURE_OWIN; @@ -25,11 +25,11 @@ - + - + - + @@ -202,5 +202,8 @@ SucceededJobs.cshtml + + SuspendedJobsPage.cshtml + \ No newline at end of file diff --git a/src/Hangfire.Core/IRecurringJobManager.cs b/src/Hangfire.Core/IRecurringJobManager.cs index 2b9b84485..e54d44c46 100644 --- a/src/Hangfire.Core/IRecurringJobManager.cs +++ b/src/Hangfire.Core/IRecurringJobManager.cs @@ -29,5 +29,17 @@ void AddOrUpdate( void Trigger([NotNull] string recurringJobId); void RemoveIfExists([NotNull] string recurringJobId); + + /// + /// Suspends the job from being processed. To resume call the ResumeJob method. + /// + /// + void SuspendJob(string jobId); + + /// + /// Resumes job processing for suspended jobs + /// + /// + void ResumeJob(string jobId); } } \ No newline at end of file diff --git a/src/Hangfire.Core/RecurringJobManager.cs b/src/Hangfire.Core/RecurringJobManager.cs index be425baf8..2f7fee3db 100644 --- a/src/Hangfire.Core/RecurringJobManager.cs +++ b/src/Hangfire.Core/RecurringJobManager.cs @@ -15,11 +15,12 @@ // License along with Hangfire. If not, see . using System; +using System.Linq; using Hangfire.Annotations; using Hangfire.Client; using Hangfire.Common; using Hangfire.Logging; -using Hangfire.Profiling; +using Hangfire.Profiling;// namespace Hangfire { @@ -54,7 +55,7 @@ public RecurringJobManager([NotNull] JobStorage storage, [NotNull] IJobFilterPro } public RecurringJobManager( - [NotNull] JobStorage storage, + [NotNull] JobStorage storage, [NotNull] IJobFilterProvider filterProvider, [NotNull] ITimeZoneResolver timeZoneResolver) : this(storage, filterProvider, timeZoneResolver, () => DateTime.UtcNow) @@ -62,8 +63,8 @@ public RecurringJobManager( } public RecurringJobManager( - [NotNull] JobStorage storage, - [NotNull] IJobFilterProvider filterProvider, + [NotNull] JobStorage storage, + [NotNull] IJobFilterProvider filterProvider, [NotNull] ITimeZoneResolver timeZoneResolver, [NotNull] Func nowFactory) : this(storage, new BackgroundJobFactory(filterProvider), timeZoneResolver, nowFactory) @@ -81,7 +82,7 @@ public RecurringJobManager([NotNull] JobStorage storage, [NotNull] IBackgroundJo } internal RecurringJobManager( - [NotNull] JobStorage storage, + [NotNull] JobStorage storage, [NotNull] IBackgroundJobFactory factory, [NotNull] ITimeZoneResolver timeZoneResolver, [NotNull] Func nowFactory) @@ -121,7 +122,7 @@ public void AddOrUpdate(string recurringJobId, Job job, string cronExpression, R } } } - + private static void ValidateCronExpression(string cronExpression) { try @@ -193,5 +194,56 @@ public void RemoveIfExists(string recurringJobId) transaction.Commit(); } } + + public void SuspendJob(string recurringJobId) + { + if (recurringJobId == null) throw new ArgumentNullException(nameof(recurringJobId)); + + using (var connection = _storage.GetConnection()) + using (connection.AcquireDistributedRecurringJobLock(recurringJobId, DefaultTimeout)) + { + var now = _nowFactory(); + var recurringJob = connection.GetRecurringJob(recurringJobId, _timeZoneResolver, now); + using (var transaction = connection.CreateWriteTransaction()) + { + transaction.AddToSet("paused-jobs", recurringJobId, JobHelper.ToTimestamp(DateTime.UtcNow)); + transaction.RemoveFromSet("recurring-jobs", recurringJobId); + transaction.Commit(); + } + } + } + + public void ResumeJob(string recurringJobId) + { + if (recurringJobId == null) + throw new ArgumentNullException(nameof(recurringJobId)); + + var monitor = _storage.GetMonitoringApi(); + var job = monitor.JobDetails(recurringJobId); + string jd = ""; + if (job != null && job.Job != null) + { + foreach (var RecurringJobId in job.Properties.Where(x => x.Key == "RecurringJobId")) + { + jd = RecurringJobId.Value; + } + } + + if (!string.IsNullOrEmpty(jd)) + recurringJobId = jd.Replace("\"", ""); + + using (var connection = _storage.GetConnection()) + { + using (connection.AcquireDistributedRecurringJobLock(recurringJobId, DefaultTimeout)) + { + using (var transaction = connection.CreateWriteTransaction()) + { + transaction.RemoveFromSet("paused-jobs", recurringJobId); + transaction.AddToSet("recurring-jobs", recurringJobId, JobHelper.ToTimestamp(DateTime.UtcNow)); + transaction.Commit(); + } + } + } + } } } diff --git a/src/Hangfire.Core/Server/PerformContext.cs b/src/Hangfire.Core/Server/PerformContext.cs index 861656c98..abce24140 100644 --- a/src/Hangfire.Core/Server/PerformContext.cs +++ b/src/Hangfire.Core/Server/PerformContext.cs @@ -60,16 +60,11 @@ internal PerformContext( [NotNull] IJobCancellationToken cancellationToken, [NotNull] IProfiler profiler) { - if (connection == null) throw new ArgumentNullException(nameof(connection)); - if (backgroundJob == null) throw new ArgumentNullException(nameof(backgroundJob)); - if (cancellationToken == null) throw new ArgumentNullException(nameof(cancellationToken)); - if (profiler == null) throw new ArgumentNullException(nameof(profiler)); - Storage = storage; - Connection = connection; - BackgroundJob = backgroundJob; - CancellationToken = cancellationToken; - Profiler = profiler; + Connection = connection ?? throw new ArgumentNullException(nameof(connection)); + BackgroundJob = backgroundJob ?? throw new ArgumentNullException(nameof(backgroundJob)); + CancellationToken = cancellationToken ?? throw new ArgumentNullException(nameof(cancellationToken)); + Profiler = profiler ?? throw new ArgumentNullException(nameof(profiler)); Items = new Dictionary(); } diff --git a/src/Hangfire.Core/Server/Worker.cs b/src/Hangfire.Core/Server/Worker.cs index b3194e5e5..f4ea6f0c2 100644 --- a/src/Hangfire.Core/Server/Worker.cs +++ b/src/Hangfire.Core/Server/Worker.cs @@ -115,6 +115,7 @@ public void Execute(BackgroundProcessContext context) connection, fetchedJob, processingState, + null, new[] { EnqueuedState.StateName, ProcessingState.StateName }, linkedCts.Token, context.StoppingToken); @@ -135,12 +136,20 @@ public void Execute(BackgroundProcessContext context) // it was performed to guarantee that it was performed AT LEAST once. // It will be re-queued after the JobTimeout was expired. - var state = PerformJob(context, connection, fetchedJob.JobId); + var state = PerformJob(context, connection, fetchedJob.JobId, out var customData); if (state != null) { // Ignore return value, because we should not do anything when current state is not Processing. - TryChangeState(context, connection, fetchedJob, state, new[] { ProcessingState.StateName }, CancellationToken.None, context.ShutdownToken); + TryChangeState( + context, + connection, + fetchedJob, + state, + customData, + new[] { ProcessingState.StateName }, + CancellationToken.None, + context.ShutdownToken); } // Checkpoint #4. The job was performed, and it is in the one @@ -177,6 +186,7 @@ private IState TryChangeState( IStorageConnection connection, IFetchedJob fetchedJob, IState state, + IReadOnlyDictionary customData, string[] expectedStates, CancellationToken initializeToken, CancellationToken abortToken) @@ -197,7 +207,8 @@ private IState TryChangeState( expectedStates, disableFilters: false, initializeToken, - _profiler)); + _profiler, + customData)); } catch (Exception ex) { @@ -239,8 +250,14 @@ private void Requeue(IFetchedJob fetchedJob) } } - private IState PerformJob(BackgroundProcessContext context, IStorageConnection connection, string jobId) + private IState PerformJob( + BackgroundProcessContext context, + IStorageConnection connection, + string jobId, + out IReadOnlyDictionary customData) { + customData = null; + try { var jobData = connection.GetJobData(jobId); @@ -267,6 +284,7 @@ private IState PerformJob(BackgroundProcessContext context, IStorageConnection c var result = _performer.Perform(performContext); duration.Stop(); + customData = new Dictionary(performContext.Items); return new SucceededState(result, (long) latency, duration.ElapsedMilliseconds); } } diff --git a/src/Hangfire.Core/States/ApplyStateContext.cs b/src/Hangfire.Core/States/ApplyStateContext.cs index df1d17ddf..13f2eb800 100644 --- a/src/Hangfire.Core/States/ApplyStateContext.cs +++ b/src/Hangfire.Core/States/ApplyStateContext.cs @@ -15,6 +15,7 @@ // License along with Hangfire. If not, see . using System; +using System.Collections.Generic; using Hangfire.Annotations; using Hangfire.Profiling; using Hangfire.Storage; @@ -28,8 +29,9 @@ public class ApplyStateContext : StateContext public ApplyStateContext( [NotNull] IWriteOnlyTransaction transaction, [NotNull] ElectStateContext context) - : this(context.Storage, context.Connection, transaction, context.BackgroundJob, context.CandidateState, context.CurrentState, context.Profiler) + : this(context.Storage, context.Connection, transaction, context.BackgroundJob, context.CandidateState, context.CurrentState, context.Profiler, context.CustomData != null ? new Dictionary(context.CustomData) : null) { + // TODO: Add explicit JobExpirationTimeout parameter in 2.0, because it's unclear it isn't preserved } public ApplyStateContext( @@ -50,23 +52,18 @@ internal ApplyStateContext( [NotNull] BackgroundJob backgroundJob, [NotNull] IState newState, [CanBeNull] string oldStateName, - [NotNull] IProfiler profiler) + [NotNull] IProfiler profiler, + [CanBeNull] IReadOnlyDictionary customData = null) { - if (storage == null) throw new ArgumentNullException(nameof(storage)); - 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; - Connection = connection; - Transaction = transaction; + BackgroundJob = backgroundJob ?? throw new ArgumentNullException(nameof(backgroundJob)); + Storage = storage ?? throw new ArgumentNullException(nameof(storage)); + Connection = connection ?? throw new ArgumentNullException(nameof(connection)); + Transaction = transaction ?? throw new ArgumentNullException(nameof(transaction)); + NewState = newState ?? throw new ArgumentNullException(nameof(newState)); OldStateName = oldStateName; - NewState = newState; + Profiler = profiler ?? throw new ArgumentNullException(nameof(profiler)); + CustomData = customData; JobExpirationTimeout = storage.JobExpirationTimeout; - Profiler = profiler; } [NotNull] @@ -90,5 +87,8 @@ internal ApplyStateContext( [NotNull] internal IProfiler Profiler { get; } + + [CanBeNull] + public IReadOnlyDictionary CustomData { get; } } } \ No newline at end of file diff --git a/src/Hangfire.Core/States/BackgroundJobStateChanger.cs b/src/Hangfire.Core/States/BackgroundJobStateChanger.cs index f1cd1ccc7..6c76209a8 100644 --- a/src/Hangfire.Core/States/BackgroundJobStateChanger.cs +++ b/src/Hangfire.Core/States/BackgroundJobStateChanger.cs @@ -104,7 +104,8 @@ public IState ChangeState(StateChangeContext context) new BackgroundJob(context.BackgroundJobId, jobData.Job, jobData.CreatedAt), stateToApply, jobData.State, - context.Profiler); + context.Profiler, + context.CustomData); // State changing process can fail due to an exception in state filters themselves, // and DisableFilters property will cause state machine to perform a state transition diff --git a/src/Hangfire.Core/States/ElectStateContext.cs b/src/Hangfire.Core/States/ElectStateContext.cs index 4d4bafdce..15fdec63b 100644 --- a/src/Hangfire.Core/States/ElectStateContext.cs +++ b/src/Hangfire.Core/States/ElectStateContext.cs @@ -43,6 +43,7 @@ public ElectStateContext([NotNull] ApplyStateContext applyContext) Transaction = applyContext.Transaction; CurrentState = applyContext.OldStateName; Profiler = applyContext.Profiler; + CustomData = applyContext.CustomData?.ToDictionary(x => x.Key, x => x.Value); } public override BackgroundJob BackgroundJob { get; } @@ -59,7 +60,7 @@ public ElectStateContext([NotNull] ApplyStateContext applyContext) [NotNull] public IState CandidateState { - get { return _candidateState; } + get => _candidateState; set { if (value == null) @@ -76,7 +77,7 @@ public IState CandidateState } [CanBeNull] - public string CurrentState { get; private set; } + public string CurrentState { get; } [NotNull] public IState[] TraversedStates => _traversedStates.ToArray(); @@ -84,6 +85,9 @@ public IState CandidateState [NotNull] internal IProfiler Profiler { get; } + [CanBeNull] + public IDictionary CustomData { get; } + public void SetJobParameter(string name, T value) { Connection.SetJobParameter(BackgroundJob.Id, name, SerializationHelper.Serialize(value, SerializationOption.User)); diff --git a/src/Hangfire.Core/States/StateChangeContext.cs b/src/Hangfire.Core/States/StateChangeContext.cs index 1c74af6e5..e8df3cacd 100644 --- a/src/Hangfire.Core/States/StateChangeContext.cs +++ b/src/Hangfire.Core/States/StateChangeContext.cs @@ -63,22 +63,18 @@ internal StateChangeContext( [CanBeNull] IEnumerable expectedStates, bool disableFilters, CancellationToken cancellationToken, - [NotNull] IProfiler profiler) + [NotNull] IProfiler profiler, + [CanBeNull] IReadOnlyDictionary customData = null) { - if (storage == null) throw new ArgumentNullException(nameof(storage)); - 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; - Connection = connection; - BackgroundJobId = backgroundJobId; - NewState = newState; + Storage = storage ?? throw new ArgumentNullException(nameof(storage)); + Connection = connection ?? throw new ArgumentNullException(nameof(connection)); + BackgroundJobId = backgroundJobId ?? throw new ArgumentNullException(nameof(backgroundJobId)); + NewState = newState ?? throw new ArgumentNullException(nameof(newState)); ExpectedStates = expectedStates; DisableFilters = disableFilters; CancellationToken = cancellationToken; - Profiler = profiler; + Profiler = profiler ?? throw new ArgumentNullException(nameof(profiler)); + CustomData = customData; } public JobStorage Storage { get; } @@ -89,5 +85,6 @@ internal StateChangeContext( public bool DisableFilters { get; } public CancellationToken CancellationToken { get; } internal IProfiler Profiler { get; } + public IReadOnlyDictionary CustomData { get; } } } \ No newline at end of file diff --git a/src/Hangfire.Core/Storage/Monitoring/StatisticsDto.cs b/src/Hangfire.Core/Storage/Monitoring/StatisticsDto.cs index 3ec922933..fbdc4eae5 100644 --- a/src/Hangfire.Core/Storage/Monitoring/StatisticsDto.cs +++ b/src/Hangfire.Core/Storage/Monitoring/StatisticsDto.cs @@ -27,5 +27,6 @@ public class StatisticsDto public long Succeeded { get; set; } public long Failed { get; set; } public long Deleted { get; set; } + public long? Retries { get; set; } } } diff --git a/src/Hangfire.SqlServer.Msmq/Hangfire.SqlServer.Msmq.csproj b/src/Hangfire.SqlServer.Msmq/Hangfire.SqlServer.Msmq.csproj index 83304bbe4..549a80ef1 100644 --- a/src/Hangfire.SqlServer.Msmq/Hangfire.SqlServer.Msmq.csproj +++ b/src/Hangfire.SqlServer.Msmq/Hangfire.SqlServer.Msmq.csproj @@ -1,6 +1,6 @@  - net45 + net48;net45 portable false true @@ -8,7 +8,7 @@ Hangfire.SqlServer.Msmq - + full diff --git a/src/Hangfire.SqlServer/Hangfire.SqlServer.csproj b/src/Hangfire.SqlServer/Hangfire.SqlServer.csproj index 5376afe51..5636109cd 100644 --- a/src/Hangfire.SqlServer/Hangfire.SqlServer.csproj +++ b/src/Hangfire.SqlServer/Hangfire.SqlServer.csproj @@ -1,6 +1,6 @@  - net45;netstandard1.3;netstandard2.0; + net48;net45;netstandard1.3;netstandard2.0; portable false true @@ -8,7 +8,7 @@ Hangfire.SqlServer - + full $(DefineConstants);FEATURE_TRANSACTIONSCOPE;FEATURE_CONFIGURATIONMANAGER @@ -30,11 +30,11 @@ - + - + diff --git a/src/Hangfire.SqlServer/SqlServerMonitoringApi.cs b/src/Hangfire.SqlServer/SqlServerMonitoringApi.cs index ead44f6da..611207c45 100644 --- a/src/Hangfire.SqlServer/SqlServerMonitoringApi.cs +++ b/src/Hangfire.SqlServer/SqlServerMonitoringApi.cs @@ -365,6 +365,7 @@ union all ) as s; select count(*) from [{0}].[Set] with (nolock, forceseek) where [Key] = N'recurring-jobs'; +select count(*) from [{0}].[Set] with (nolock, forceseek) where [Key] = N'retries'; ", _storage.SchemaName); var statistics = UseConnection(connection => @@ -383,6 +384,7 @@ union all stats.Deleted = multi.ReadSingleOrDefault() ?? 0; stats.Recurring = multi.ReadSingle(); + stats.Retries = multi.ReadSingle(); } return stats; }); diff --git a/tests/Hangfire.Core.Tests/Common/JobFilterAttributeFacts.cs b/tests/Hangfire.Core.Tests/Common/JobFilterAttributeFacts.cs index fe17d65c2..02aaa519d 100644 --- a/tests/Hangfire.Core.Tests/Common/JobFilterAttributeFacts.cs +++ b/tests/Hangfire.Core.Tests/Common/JobFilterAttributeFacts.cs @@ -10,9 +10,52 @@ public class JobFilterAttributeFacts [Fact] public void SetOrder_ThrowsAnException_WhenValueIsLessThanDefaultOrder() { - var filterAttribute = new Mock() { CallBase = true }; + var filterAttribute = new Mock { CallBase = true }; Assert.Throws( () => filterAttribute.Object.Order = -2); } + + [Fact] + public void TypeId_Property_IsNotIncludedIntoSerializedForm() + { + var attribute = new SampleJobAttribute(); + var serialized = SerializationHelper.Serialize(attribute); + Assert.DoesNotContain("TypeId", serialized); + } + + [Fact] + public void AllowMultiple_Property_IsNotIncludedIntoSerializedForm_SinceItIsGetOnlyProperty() + { + var attribute = new SampleJobAttribute(); + var serialized = SerializationHelper.Serialize(attribute); + Assert.DoesNotContain("AllowMultiple", serialized); + } + + [Fact] + public void Order_Property_IsNotIncludedIntoSerializedForm_WhenDefaultValueIsUsed() + { + var attribute = new SampleJobAttribute(); + var serialized = SerializationHelper.Serialize(attribute); + Assert.DoesNotContain("Order", serialized); + } + + [Fact] + public void Order_Property_IsIncludedIntoSerializedForm_WhenNonDefaultValueIsUsed() + { + var attribute = new SampleJobAttribute { Order = 555 }; + var serialized = SerializationHelper.Serialize(attribute); + Assert.Contains("\"Order\":555", serialized); + } + + [Fact] + public void Order_Property_ProperlyHandlesDefaultValue_WhenBeingDeserialized() + { + var attribute = SerializationHelper.Deserialize("{}"); + Assert.Equal(-1, attribute.Order); + } + + private sealed class SampleJobAttribute : JobFilterAttribute + { + } } } diff --git a/tests/Hangfire.Core.Tests/Hangfire.Core.Tests.csproj.DotSettings b/tests/Hangfire.Core.Tests/Hangfire.Core.Tests.csproj.DotSettings new file mode 100644 index 000000000..61585476a --- /dev/null +++ b/tests/Hangfire.Core.Tests/Hangfire.Core.Tests.csproj.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/tests/Hangfire.Core.Tests/Mocks/ApplyStateContextMock.cs b/tests/Hangfire.Core.Tests/Mocks/ApplyStateContextMock.cs index 025156cb2..d5172f02e 100644 --- a/tests/Hangfire.Core.Tests/Mocks/ApplyStateContextMock.cs +++ b/tests/Hangfire.Core.Tests/Mocks/ApplyStateContextMock.cs @@ -1,11 +1,13 @@ using System; +using System.Collections.Generic; +using Hangfire.Profiling; using Hangfire.States; using Hangfire.Storage; using Moq; namespace Hangfire.Core.Tests { - class ApplyStateContextMock + public class ApplyStateContextMock { private readonly Lazy _context; @@ -26,7 +28,9 @@ public ApplyStateContextMock() Transaction.Object, BackgroundJob.Object, NewStateObject ?? NewState.Object, - OldStateName) + OldStateName, + EmptyProfiler.Instance, + CustomData) { JobExpirationTimeout = JobExpirationTimeout }); @@ -39,7 +43,8 @@ public ApplyStateContextMock() 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 IReadOnlyDictionary CustomData { get; set; } public ApplyStateContext Object => _context.Value; } diff --git a/tests/Hangfire.Core.Tests/Mocks/BackgroundJobMock.cs b/tests/Hangfire.Core.Tests/Mocks/BackgroundJobMock.cs index 8482fd1a7..0f59b0df2 100644 --- a/tests/Hangfire.Core.Tests/Mocks/BackgroundJobMock.cs +++ b/tests/Hangfire.Core.Tests/Mocks/BackgroundJobMock.cs @@ -3,7 +3,7 @@ namespace Hangfire.Core.Tests { - class BackgroundJobMock + public class BackgroundJobMock { private readonly Lazy _object; diff --git a/tests/Hangfire.Core.Tests/Mocks/CreateContextMock.cs b/tests/Hangfire.Core.Tests/Mocks/CreateContextMock.cs index bbe2524f6..cc23e5cc4 100644 --- a/tests/Hangfire.Core.Tests/Mocks/CreateContextMock.cs +++ b/tests/Hangfire.Core.Tests/Mocks/CreateContextMock.cs @@ -7,7 +7,7 @@ namespace Hangfire.Core.Tests { - class CreateContextMock + public class CreateContextMock { private readonly Lazy _context; diff --git a/tests/Hangfire.Core.Tests/Mocks/ElectStateContextMock.cs b/tests/Hangfire.Core.Tests/Mocks/ElectStateContextMock.cs index 86be62beb..8accfb5b7 100644 --- a/tests/Hangfire.Core.Tests/Mocks/ElectStateContextMock.cs +++ b/tests/Hangfire.Core.Tests/Mocks/ElectStateContextMock.cs @@ -3,7 +3,7 @@ namespace Hangfire.Core.Tests { - class ElectStateContextMock + public class ElectStateContextMock { private readonly Lazy _context; diff --git a/tests/Hangfire.Core.Tests/Mocks/PerformContextMock.cs b/tests/Hangfire.Core.Tests/Mocks/PerformContextMock.cs index 1461f2606..81dca31ee 100644 --- a/tests/Hangfire.Core.Tests/Mocks/PerformContextMock.cs +++ b/tests/Hangfire.Core.Tests/Mocks/PerformContextMock.cs @@ -5,7 +5,7 @@ namespace Hangfire.Core.Tests { - class PerformContextMock + public class PerformContextMock { private readonly Lazy _context; diff --git a/tests/Hangfire.Core.Tests/Mocks/StateChangeContextMock.cs b/tests/Hangfire.Core.Tests/Mocks/StateChangeContextMock.cs index 2eed3a88a..5afe70743 100644 --- a/tests/Hangfire.Core.Tests/Mocks/StateChangeContextMock.cs +++ b/tests/Hangfire.Core.Tests/Mocks/StateChangeContextMock.cs @@ -8,7 +8,7 @@ namespace Hangfire.Core.Tests { - class StateChangeContextMock + public class StateChangeContextMock { private readonly Lazy _context; @@ -29,9 +29,9 @@ public StateChangeContextMock() BackgroundJobId, NewState.Object, ExpectedStates, - DisableFilters, CancellationToken, - EmptyProfiler.Instance)); + EmptyProfiler.Instance, + CustomData)); } public Mock Storage { get; set; } @@ -41,6 +41,7 @@ public StateChangeContextMock() public IEnumerable ExpectedStates { get; set; } public bool DisableFilters { get; set; } public CancellationToken CancellationToken { get; set; } + public IReadOnlyDictionary CustomData { get; set; } public StateChangeContext Object => _context.Value; } diff --git a/tests/Hangfire.Core.Tests/Server/WorkerFacts.cs b/tests/Hangfire.Core.Tests/Server/WorkerFacts.cs index cd9bde18f..5f49dc192 100644 --- a/tests/Hangfire.Core.Tests/Server/WorkerFacts.cs +++ b/tests/Hangfire.Core.Tests/Server/WorkerFacts.cs @@ -298,6 +298,20 @@ ctx.NewState is SucceededState && ctx.ExpectedStates.ElementAt(0) == ProcessingState.StateName))); } + [Fact] + public void Execute_PassesCustomData_BetweenContexts_OnSucceededStateTransition() + { + var worker = CreateWorker(); + _performer.Setup(x => x.Perform(It.IsNotNull())) + .Callback(ctx => ctx.Items.Add("Key", "Value")); + + worker.Execute(_context.Object); + + _stateChanger.Verify(x => x.ChangeState(It.Is(ctx => + ctx.NewState is SucceededState && + ctx.CustomData["Key"].Equals("Value")))); + } + [Fact] public void Execute_MovesJob_ToFailedState_IfThereWasInternalException() { @@ -320,6 +334,21 @@ ctx.NewState is FailedState && ctx.DisableFilters == false))); } + [Fact] + public void Execute_DoesNotPassCustomData_BetweenContexts_OnFailedStateTransition() + { + var worker = CreateWorker(); + _performer.Setup(x => x.Perform(It.IsNotNull())) + .Callback(ctx => ctx.Items.Add("Key", "Value")) + .Throws(); + + worker.Execute(_context.Object); + + _stateChanger.Verify(x => x.ChangeState(It.Is(ctx => + ctx.NewState is FailedState && + ctx.CustomData == null))); + } + [Fact] public void Execute_MovesJob_ToFailedState_IfThereWasUserException() { diff --git a/tests/Hangfire.Core.Tests/States/ApplyStateContextFacts.cs b/tests/Hangfire.Core.Tests/States/ApplyStateContextFacts.cs index 4bcf25366..f71de6535 100644 --- a/tests/Hangfire.Core.Tests/States/ApplyStateContextFacts.cs +++ b/tests/Hangfire.Core.Tests/States/ApplyStateContextFacts.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using Hangfire.Profiling; using Hangfire.States; using Hangfire.Storage; using Moq; @@ -18,6 +20,8 @@ public class ApplyStateContextFacts private readonly BackgroundJobMock _backgroundJob; private readonly Mock _transaction; private readonly Mock _connection; + private readonly Mock _profiler; + private readonly Mock> _customData; public ApplyStateContextFacts() { @@ -27,6 +31,8 @@ public ApplyStateContextFacts() _backgroundJob = new BackgroundJobMock(); _newState = new Mock(); _newState.Setup(x => x.Name).Returns(NewState); + _profiler = new Mock(); + _customData = new Mock>(); } [Fact] @@ -77,6 +83,22 @@ public void Ctor_ThrowsAnException_WhenNewStateIsNull() Assert.Equal("newState", exception.ParamName); } + [Fact] + public void Ctor_ThrowsAnException_WhenProfilerIsNull() + { + var exception = Assert.Throws( + () => new ApplyStateContext( + _storage.Object, + _connection.Object, + _transaction.Object, + _backgroundJob.Object, + _newState.Object, + OldState, + null)); + + Assert.Equal("profiler", exception.ParamName); + } + [Fact] public void Ctor_ShouldSetPropertiesCorrectly() { @@ -96,5 +118,58 @@ public void Ctor_ShouldSetPropertiesCorrectly() Assert.Same(_newState.Object, context.NewState); Assert.Equal(_storage.Object.JobExpirationTimeout, context.JobExpirationTimeout); } + + [Fact] + public void InternalCtor_CorrectlySetsAllTheProperties() + { + var context = new ApplyStateContext( + _storage.Object, + _connection.Object, + _transaction.Object, + _backgroundJob.Object, + _newState.Object, + OldState, + _profiler.Object, + _customData.Object); + + Assert.Same(_storage.Object, context.Storage); + Assert.Same(_connection.Object, context.Connection); + Assert.Same(_transaction.Object, context.Transaction); + Assert.Same(_backgroundJob.Object, context.BackgroundJob); + Assert.Equal(OldState, context.OldStateName); + Assert.Same(_newState.Object, context.NewState); + Assert.Equal(_storage.Object.JobExpirationTimeout, context.JobExpirationTimeout); + Assert.Same(_customData.Object, context.CustomData); + } + + [Fact] + public void CopyCtor_ForElectStateContext_CorrectlySetsAllTheProperties() + { + var electContext = new ElectStateContextMock(); + var context = new ApplyStateContext(_transaction.Object, electContext.Object); + + Assert.Same(electContext.Object.Storage, context.Storage); + Assert.Same(electContext.Object.Connection, context.Connection); + Assert.Same(_transaction.Object, context.Transaction); + Assert.Same(electContext.Object.BackgroundJob, context.BackgroundJob); + Assert.Equal(electContext.Object.CurrentState, context.OldStateName); + Assert.Same(electContext.Object.CandidateState, context.NewState); + Assert.Null(context.CustomData); + } + + [Fact] + public void CopyCtor_ForElectStateContext_CopiesCustomData_ToAnotherDictionary() + { + // Arrange + var dictionary = new Dictionary { { "lalala", new object() } }; + var electContext = new ElectStateContextMock { ApplyContext = { CustomData = dictionary } }; + + // Act + var context = new ApplyStateContext(_transaction.Object, electContext.Object); + + // Assert + Assert.Equal(electContext.Object.CustomData, context.CustomData); + Assert.NotSame(electContext.Object.CustomData, context.CustomData); + } } } diff --git a/tests/Hangfire.Core.Tests/States/BackgroundJobStateChangerFacts.cs b/tests/Hangfire.Core.Tests/States/BackgroundJobStateChangerFacts.cs index d7612bbca..90d713656 100644 --- a/tests/Hangfire.Core.Tests/States/BackgroundJobStateChangerFacts.cs +++ b/tests/Hangfire.Core.Tests/States/BackgroundJobStateChangerFacts.cs @@ -69,7 +69,11 @@ public BackgroundJobStateChangerFacts() Connection = _connection, CancellationToken = _cts.Token, NewState = _state, - ExpectedStates = FromOldState + ExpectedStates = FromOldState, + CustomData = new Dictionary + { + { "Key", "Value" } + } }; _stateMachine.Setup(x => x.ApplyState(It.IsNotNull())) @@ -122,6 +126,17 @@ public void TryToChangeState_ChangesTheStateOfTheJob() Assert.Equal(_state.Object.Name, result.Name); } + [Fact] + public void ChangeState_PassesCustomData_ToApplyStateContext() + { + var stateChanger = CreateStateChanger(); + + stateChanger.ChangeState(_context.Object); + + _stateMachine.Verify(x => x.ApplyState(It.Is( + sc => sc.CustomData["Key"].Equals("Value")))); + } + [Fact] public void ChangeState_ChangesTheStateOfTheJob_WhenFromStatesIsNull() { diff --git a/tests/Hangfire.Core.Tests/States/ElectStateContextFacts.cs b/tests/Hangfire.Core.Tests/States/ElectStateContextFacts.cs index 9df521fe1..e81720ff8 100644 --- a/tests/Hangfire.Core.Tests/States/ElectStateContextFacts.cs +++ b/tests/Hangfire.Core.Tests/States/ElectStateContextFacts.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Hangfire.Common; using Hangfire.States; using Moq; @@ -38,6 +39,24 @@ public void Ctor_CorrectlySetAllProperties() Assert.Same(_applyContext.NewState.Object, context.CandidateState); Assert.Equal("State", context.CurrentState); Assert.Empty(context.TraversedStates); + Assert.Null(context.CustomData); + } + + [Fact] + public void Ctor_CopiesTheCustomDataDictionary_ToAnotherInstance() + { + // Arrange + _applyContext.CustomData = new Dictionary + { + { "lalala", new object() } + }; + + // Act + var context = CreateContext(); + + // Assert + Assert.Equal(_applyContext.CustomData, context.CustomData); + Assert.NotSame(_applyContext.CustomData, context.CustomData); } [Fact] diff --git a/tests/Hangfire.Core.Tests/States/StateChangeContextFacts.cs b/tests/Hangfire.Core.Tests/States/StateChangeContextFacts.cs new file mode 100644 index 000000000..19ddd9aed --- /dev/null +++ b/tests/Hangfire.Core.Tests/States/StateChangeContextFacts.cs @@ -0,0 +1,191 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using Hangfire.Profiling; +using Hangfire.States; +using Hangfire.Storage; +using Moq; +using Xunit; + +// ReSharper disable AssignNullToNotNullAttribute + +namespace Hangfire.Core.Tests.States +{ + public class StateChangeContextFacts + { + private const string JobId = "SomeJob"; + + private readonly Mock _storage; + private readonly Mock _connection; + private readonly Mock _newState; + private readonly string[] _expectedStates; + private readonly CancellationToken _token; + private readonly Mock _profiler; + private readonly Dictionary _customData; + + public StateChangeContextFacts() + { + _storage = new Mock(); + _connection = new Mock(); + _newState = new Mock(); + _expectedStates = new[] { "Succeeded", "Failed" }; + _token = new CancellationToken(true); + _profiler = new Mock(); + _customData = new Dictionary(); + } + + [Fact] + public void Ctor_ThrowsAnException_WhenStorageIsNull() + { + var exception = Assert.Throws( + () => new StateChangeContext( + null, + _connection.Object, + JobId, + _newState.Object)); + + Assert.Equal("storage", exception.ParamName); + } + + [Fact] + public void Ctor_ThrowsAnException_WhenConnectionIsNull() + { + var exception = Assert.Throws( + () => new StateChangeContext( + _storage.Object, + null, + JobId, + _newState.Object)); + + Assert.Equal("connection", exception.ParamName); + } + + [Fact] + public void Ctor_ThrowsAnException_WhenBackgroundJobIdIsNull() + { + var exception = Assert.Throws( + () => new StateChangeContext( + _storage.Object, + _connection.Object, + null, + _newState.Object)); + + Assert.Equal("backgroundJobId", exception.ParamName); + } + + [Fact] + public void Ctor_ThrowsAnException_WhenNewStateIsNull() + { + var exception = Assert.Throws( + () => new StateChangeContext( + _storage.Object, + _connection.Object, + JobId, + null)); + + Assert.Equal("newState", exception.ParamName); + } + + [Fact] + public void Ctor_ThrowsAnException_WhenProfilerIsNull() + { + var exception = Assert.Throws( + () => new StateChangeContext( + _storage.Object, + _connection.Object, + JobId, + _newState.Object, + null, + false, + CancellationToken.None, + null)); + + Assert.Equal("profiler", exception.ParamName); + } + + [Fact] + public void Ctor1_CorrectlySets_AllTheProperties() + { + var context = new StateChangeContext( + _storage.Object, + _connection.Object, + JobId, + _newState.Object); + + Assert.Same(_storage.Object, context.Storage); + Assert.Same(_connection.Object, context.Connection); + Assert.Equal(JobId, context.BackgroundJobId); + Assert.Same(_newState.Object, context.NewState); + Assert.Null(context.ExpectedStates); + Assert.Equal(CancellationToken.None, context.CancellationToken); + Assert.NotNull(context.Profiler); + Assert.Null(context.CustomData); + } + + [Fact] + public void Ctor2_CorrectlySets_AllTheProperties() + { + var context = new StateChangeContext( + _storage.Object, + _connection.Object, + JobId, + _newState.Object, + _expectedStates); + + Assert.Same(_storage.Object, context.Storage); + Assert.Same(_connection.Object, context.Connection); + Assert.Equal(JobId, context.BackgroundJobId); + Assert.Same(_newState.Object, context.NewState); + Assert.Equal(_expectedStates, context.ExpectedStates); + Assert.Equal(CancellationToken.None, context.CancellationToken); + Assert.NotNull(context.Profiler); + Assert.Null(context.CustomData); + } + + [Fact] + public void Ctor3_CorrectlySets_AllTheProperties() + { + var context = new StateChangeContext( + _storage.Object, + _connection.Object, + JobId, + _newState.Object, + _expectedStates, + _token); + + Assert.Same(_storage.Object, context.Storage); + Assert.Same(_connection.Object, context.Connection); + Assert.Equal(JobId, context.BackgroundJobId); + Assert.Same(_newState.Object, context.NewState); + Assert.Equal(_expectedStates, context.ExpectedStates); + Assert.Equal(_token, context.CancellationToken); + Assert.NotNull(context.Profiler); + Assert.Null(context.CustomData); + } + + [Fact] + public void InternalCtor_CorrectlySets_AllTheProperties() + { + var context = new StateChangeContext( + _storage.Object, + _connection.Object, + JobId, + _newState.Object, + _expectedStates, + false, + _token, + _profiler.Object, + _customData); + + Assert.Same(_storage.Object, context.Storage); + Assert.Same(_connection.Object, context.Connection); + Assert.Equal(JobId, context.BackgroundJobId); + Assert.Same(_newState.Object, context.NewState); + Assert.Equal(_expectedStates, context.ExpectedStates); + Assert.False(context.DisableFilters); + Assert.Equal(_token, context.CancellationToken); + Assert.Same(_profiler.Object, context.Profiler); + Assert.Same(_customData, context.CustomData); + } + } +}