From eea6db16b3c773e85a41391bc753721d45c6a27f Mon Sep 17 00:00:00 2001 From: Sergey Odinokov Date: Tue, 7 Apr 2020 08:32:38 +0300 Subject: [PATCH 01/19] Allow to pass custom data to ApplyStateContext instances --- src/Hangfire.Core/States/ApplyStateContext.cs | 8 ++++- .../States/ApplyStateContextFacts.cs | 29 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/Hangfire.Core/States/ApplyStateContext.cs b/src/Hangfire.Core/States/ApplyStateContext.cs index df1d17ddf..a872bb04d 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; @@ -50,7 +51,8 @@ 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)); @@ -67,6 +69,7 @@ internal ApplyStateContext( NewState = newState; JobExpirationTimeout = storage.JobExpirationTimeout; Profiler = profiler; + CustomData = customData; } [NotNull] @@ -90,5 +93,8 @@ internal ApplyStateContext( [NotNull] internal IProfiler Profiler { get; } + + [CanBeNull] + public IReadOnlyDictionary CustomData { get; } } } \ No newline at end of file diff --git a/tests/Hangfire.Core.Tests/States/ApplyStateContextFacts.cs b/tests/Hangfire.Core.Tests/States/ApplyStateContextFacts.cs index 4bcf25366..3058c901f 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] @@ -96,5 +102,28 @@ 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); + } } } From d2be67e92b01006e46622a84473ea572e004d3bb Mon Sep 17 00:00:00 2001 From: Sergey Odinokov Date: Tue, 7 Apr 2020 08:34:02 +0300 Subject: [PATCH 02/19] Add missing null check to the ApplyStateContext class --- src/Hangfire.Core/States/ApplyStateContext.cs | 3 ++- .../States/ApplyStateContextFacts.cs | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/Hangfire.Core/States/ApplyStateContext.cs b/src/Hangfire.Core/States/ApplyStateContext.cs index a872bb04d..1e6858c6f 100644 --- a/src/Hangfire.Core/States/ApplyStateContext.cs +++ b/src/Hangfire.Core/States/ApplyStateContext.cs @@ -59,7 +59,8 @@ internal ApplyStateContext( if (transaction == null) throw new ArgumentNullException(nameof(transaction)); if (backgroundJob == null) throw new ArgumentNullException(nameof(backgroundJob)); if (newState == null) throw new ArgumentNullException(nameof(newState)); - + if (profiler == null) throw new ArgumentNullException(nameof(profiler)); + BackgroundJob = backgroundJob; Storage = storage; diff --git a/tests/Hangfire.Core.Tests/States/ApplyStateContextFacts.cs b/tests/Hangfire.Core.Tests/States/ApplyStateContextFacts.cs index 3058c901f..967499659 100644 --- a/tests/Hangfire.Core.Tests/States/ApplyStateContextFacts.cs +++ b/tests/Hangfire.Core.Tests/States/ApplyStateContextFacts.cs @@ -83,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() { From b49eec53a23dbd714258004d0098be2afa5de083 Mon Sep 17 00:00:00 2001 From: Sergey Odinokov Date: Tue, 7 Apr 2020 08:36:56 +0300 Subject: [PATCH 03/19] Add CustomData property to the ElectStateContext class --- src/Hangfire.Core/States/ElectStateContext.cs | 4 ++++ .../Mocks/ApplyStateContextMock.cs | 9 +++++++-- .../States/ElectStateContextFacts.cs | 19 +++++++++++++++++++ 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/Hangfire.Core/States/ElectStateContext.cs b/src/Hangfire.Core/States/ElectStateContext.cs index 4d4bafdce..1d0684f8e 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; } @@ -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/tests/Hangfire.Core.Tests/Mocks/ApplyStateContextMock.cs b/tests/Hangfire.Core.Tests/Mocks/ApplyStateContextMock.cs index 025156cb2..756fb21ea 100644 --- a/tests/Hangfire.Core.Tests/Mocks/ApplyStateContextMock.cs +++ b/tests/Hangfire.Core.Tests/Mocks/ApplyStateContextMock.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using Hangfire.Profiling; using Hangfire.States; using Hangfire.Storage; using Moq; @@ -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/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] From 6faf8de0a2fdabeb707b8478614dac14e8a76f33 Mon Sep 17 00:00:00 2001 From: Sergey Odinokov Date: Tue, 7 Apr 2020 08:47:54 +0300 Subject: [PATCH 04/19] Ensure custom data is preserved between apply/elect context conversions --- src/Hangfire.Core/States/ApplyStateContext.cs | 3 +- .../States/ApplyStateContextFacts.cs | 30 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/Hangfire.Core/States/ApplyStateContext.cs b/src/Hangfire.Core/States/ApplyStateContext.cs index 1e6858c6f..7ab66ac3b 100644 --- a/src/Hangfire.Core/States/ApplyStateContext.cs +++ b/src/Hangfire.Core/States/ApplyStateContext.cs @@ -29,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( diff --git a/tests/Hangfire.Core.Tests/States/ApplyStateContextFacts.cs b/tests/Hangfire.Core.Tests/States/ApplyStateContextFacts.cs index 967499659..f71de6535 100644 --- a/tests/Hangfire.Core.Tests/States/ApplyStateContextFacts.cs +++ b/tests/Hangfire.Core.Tests/States/ApplyStateContextFacts.cs @@ -141,5 +141,35 @@ public void InternalCtor_CorrectlySetsAllTheProperties() 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); + } } } From 4b073a9b38f1e76f37da5eed449a5877fb336d59 Mon Sep 17 00:00:00 2001 From: Sergey Odinokov Date: Tue, 7 Apr 2020 10:19:53 +0300 Subject: [PATCH 05/19] Add missing unit tests for the StateChangeContext class --- .../States/StateChangeContextFacts.cs | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 tests/Hangfire.Core.Tests/States/StateChangeContextFacts.cs diff --git a/tests/Hangfire.Core.Tests/States/StateChangeContextFacts.cs b/tests/Hangfire.Core.Tests/States/StateChangeContextFacts.cs new file mode 100644 index 000000000..4cd318fde --- /dev/null +++ b/tests/Hangfire.Core.Tests/States/StateChangeContextFacts.cs @@ -0,0 +1,180 @@ +using System; +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; + + public StateChangeContextFacts() + { + _storage = new Mock(); + _connection = new Mock(); + _newState = new Mock(); + _expectedStates = new[] { "Succeeded", "Failed" }; + _token = new CancellationToken(true); + _profiler = new Mock(); + } + + [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, + 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); + } + + [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); + } + + [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); + } + + [Fact] + public void InternalCtor_CorrectlySets_AllTheProperties() + { + var context = new StateChangeContext( + _storage.Object, + _connection.Object, + JobId, + _newState.Object, + _expectedStates, + _token, + _profiler.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.Equal(_expectedStates, context.ExpectedStates); + Assert.Equal(_token, context.CancellationToken); + Assert.Same(_profiler.Object, context.Profiler); + } + } +} From 58b17e81cc7731d38074e5cf254b936ca257ef4d Mon Sep 17 00:00:00 2001 From: Sergey Odinokov Date: Tue, 7 Apr 2020 10:22:05 +0300 Subject: [PATCH 06/19] Add custom data property to the StateChangeContext class --- src/Hangfire.Core/States/StateChangeContext.cs | 5 ++++- .../States/StateChangeContextFacts.cs | 10 +++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/Hangfire.Core/States/StateChangeContext.cs b/src/Hangfire.Core/States/StateChangeContext.cs index fa3806cee..7526ba8ed 100644 --- a/src/Hangfire.Core/States/StateChangeContext.cs +++ b/src/Hangfire.Core/States/StateChangeContext.cs @@ -62,7 +62,8 @@ internal StateChangeContext( [NotNull] IState newState, [CanBeNull] IEnumerable expectedStates, 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)); @@ -77,6 +78,7 @@ internal StateChangeContext( ExpectedStates = expectedStates; CancellationToken = cancellationToken; Profiler = profiler; + CustomData = customData; } public JobStorage Storage { get; } @@ -86,5 +88,6 @@ internal StateChangeContext( public IEnumerable ExpectedStates { get; } public CancellationToken CancellationToken { get; } internal IProfiler Profiler { get; } + public IReadOnlyDictionary CustomData { get; } } } \ No newline at end of file diff --git a/tests/Hangfire.Core.Tests/States/StateChangeContextFacts.cs b/tests/Hangfire.Core.Tests/States/StateChangeContextFacts.cs index 4cd318fde..b7c03c892 100644 --- a/tests/Hangfire.Core.Tests/States/StateChangeContextFacts.cs +++ b/tests/Hangfire.Core.Tests/States/StateChangeContextFacts.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading; using Hangfire.Profiling; using Hangfire.States; @@ -20,6 +21,7 @@ public class StateChangeContextFacts private readonly string[] _expectedStates; private readonly CancellationToken _token; private readonly Mock _profiler; + private readonly Dictionary _customData; public StateChangeContextFacts() { @@ -29,6 +31,7 @@ public StateChangeContextFacts() _expectedStates = new[] { "Succeeded", "Failed" }; _token = new CancellationToken(true); _profiler = new Mock(); + _customData = new Dictionary(); } [Fact] @@ -115,6 +118,7 @@ public void Ctor1_CorrectlySets_AllTheProperties() Assert.Null(context.ExpectedStates); Assert.Equal(CancellationToken.None, context.CancellationToken); Assert.NotNull(context.Profiler); + Assert.Null(context.CustomData); } [Fact] @@ -134,6 +138,7 @@ public void Ctor2_CorrectlySets_AllTheProperties() Assert.Equal(_expectedStates, context.ExpectedStates); Assert.Equal(CancellationToken.None, context.CancellationToken); Assert.NotNull(context.Profiler); + Assert.Null(context.CustomData); } [Fact] @@ -154,6 +159,7 @@ public void Ctor3_CorrectlySets_AllTheProperties() Assert.Equal(_expectedStates, context.ExpectedStates); Assert.Equal(_token, context.CancellationToken); Assert.NotNull(context.Profiler); + Assert.Null(context.CustomData); } [Fact] @@ -166,7 +172,8 @@ public void InternalCtor_CorrectlySets_AllTheProperties() _newState.Object, _expectedStates, _token, - _profiler.Object); + _profiler.Object, + _customData); Assert.Same(_storage.Object, context.Storage); Assert.Same(_connection.Object, context.Connection); @@ -175,6 +182,7 @@ public void InternalCtor_CorrectlySets_AllTheProperties() Assert.Equal(_expectedStates, context.ExpectedStates); Assert.Equal(_token, context.CancellationToken); Assert.Same(_profiler.Object, context.Profiler); + Assert.Same(_customData, context.CustomData); } } } From 21451ef44774d4df3c12f3a0ebb13ec569360746 Mon Sep 17 00:00:00 2001 From: Sergey Odinokov Date: Tue, 7 Apr 2020 10:35:11 +0300 Subject: [PATCH 07/19] Pass custom data between contexts in state changer --- .../States/BackgroundJobStateChanger.cs | 3 ++- .../Mocks/StateChangeContextMock.cs | 6 +++++- .../States/BackgroundJobStateChangerFacts.cs | 17 ++++++++++++++++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/Hangfire.Core/States/BackgroundJobStateChanger.cs b/src/Hangfire.Core/States/BackgroundJobStateChanger.cs index 37a8f70a5..54aa7ae50 100644 --- a/src/Hangfire.Core/States/BackgroundJobStateChanger.cs +++ b/src/Hangfire.Core/States/BackgroundJobStateChanger.cs @@ -119,7 +119,8 @@ public IState ChangeState(StateChangeContext context) new BackgroundJob(context.BackgroundJobId, jobData.Job, jobData.CreatedAt), stateToApply, jobData.State, - context.Profiler); + context.Profiler, + context.CustomData); var appliedState = _stateMachine.ApplyState(applyContext); diff --git a/tests/Hangfire.Core.Tests/Mocks/StateChangeContextMock.cs b/tests/Hangfire.Core.Tests/Mocks/StateChangeContextMock.cs index 0bc11f9be..8dc5fc954 100644 --- a/tests/Hangfire.Core.Tests/Mocks/StateChangeContextMock.cs +++ b/tests/Hangfire.Core.Tests/Mocks/StateChangeContextMock.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Threading; +using Hangfire.Profiling; using Hangfire.States; using Hangfire.Storage; using Moq; @@ -27,7 +28,9 @@ public StateChangeContextMock() BackgroundJobId, NewState.Object, ExpectedStates, - CancellationToken)); + CancellationToken, + EmptyProfiler.Instance, + CustomData)); } public Mock Storage { get; set; } @@ -36,6 +39,7 @@ public StateChangeContextMock() public Mock NewState { get; set; } public IEnumerable ExpectedStates { get; set; } public CancellationToken CancellationToken { get; set; } + public IReadOnlyDictionary CustomData { get; set; } public StateChangeContext Object => _context.Value; } diff --git a/tests/Hangfire.Core.Tests/States/BackgroundJobStateChangerFacts.cs b/tests/Hangfire.Core.Tests/States/BackgroundJobStateChangerFacts.cs index 77356fb14..eb901aed0 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() { From 4de10a60fa884ebaf1ed9fe624a83c38f583b2df Mon Sep 17 00:00:00 2001 From: Sergey Odinokov Date: Tue, 7 Apr 2020 14:17:46 +0300 Subject: [PATCH 08/19] Pass custom data between contexts in workers --- src/Hangfire.Core/Server/Worker.cs | 26 ++++++++++++++--- .../Hangfire.Core.Tests/Server/WorkerFacts.cs | 29 +++++++++++++++++++ 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/src/Hangfire.Core/Server/Worker.cs b/src/Hangfire.Core/Server/Worker.cs index c46e2568c..8603bacd5 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) @@ -196,7 +206,8 @@ private IState TryChangeState( state, expectedStates, initializeToken, - _profiler)); + _profiler, + customData)); } catch (Exception ex) { @@ -237,8 +248,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); @@ -265,6 +282,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/tests/Hangfire.Core.Tests/Server/WorkerFacts.cs b/tests/Hangfire.Core.Tests/Server/WorkerFacts.cs index ab6b8ba04..263ebd028 100644 --- a/tests/Hangfire.Core.Tests/Server/WorkerFacts.cs +++ b/tests/Hangfire.Core.Tests/Server/WorkerFacts.cs @@ -285,6 +285,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() { @@ -306,6 +320,21 @@ ctx.NewState is FailedState && ((FailedState) ctx.NewState).Exception == exception))); } + [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() { From 5c28ac0413908f75af8bf72f946b14cc97f4ed79 Mon Sep 17 00:00:00 2001 From: Sergey Odinokov Date: Wed, 8 Apr 2020 11:04:53 +0300 Subject: [PATCH 09/19] Update Hangfire.sln.DotSettings --- Hangfire.sln.DotSettings | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Hangfire.sln.DotSettings b/Hangfire.sln.DotSettings index 299519dbf..8386f9af1 100644 --- a/Hangfire.sln.DotSettings +++ b/Hangfire.sln.DotSettings @@ -10,4 +10,8 @@ True 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 /> \ No newline at end of file + <data /> + True + True + True + \ No newline at end of file From 0b0ec7f634fee4e54f59f8c312fcec5748e01af5 Mon Sep 17 00:00:00 2001 From: Sergey Odinokov Date: Wed, 8 Apr 2020 11:10:20 +0300 Subject: [PATCH 10/19] Make context mock classes public --- tests/Hangfire.Core.Tests/Mocks/ApplyStateContextMock.cs | 2 +- tests/Hangfire.Core.Tests/Mocks/BackgroundJobMock.cs | 2 +- tests/Hangfire.Core.Tests/Mocks/CreateContextMock.cs | 2 +- tests/Hangfire.Core.Tests/Mocks/ElectStateContextMock.cs | 2 +- tests/Hangfire.Core.Tests/Mocks/PerformContextMock.cs | 2 +- tests/Hangfire.Core.Tests/Mocks/StateChangeContextMock.cs | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/Hangfire.Core.Tests/Mocks/ApplyStateContextMock.cs b/tests/Hangfire.Core.Tests/Mocks/ApplyStateContextMock.cs index 756fb21ea..d5172f02e 100644 --- a/tests/Hangfire.Core.Tests/Mocks/ApplyStateContextMock.cs +++ b/tests/Hangfire.Core.Tests/Mocks/ApplyStateContextMock.cs @@ -7,7 +7,7 @@ namespace Hangfire.Core.Tests { - class ApplyStateContextMock + public class ApplyStateContextMock { private readonly Lazy _context; 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 8dc5fc954..1e1bb67f8 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; From 87094a281e614ad9462572251bd6304b9d2befda Mon Sep 17 00:00:00 2001 From: Sergey Odinokov Date: Wed, 8 Apr 2020 11:10:25 +0300 Subject: [PATCH 11/19] Create Hangfire.Core.Tests.csproj.DotSettings --- .../Hangfire.Core.Tests/Hangfire.Core.Tests.csproj.DotSettings | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 tests/Hangfire.Core.Tests/Hangfire.Core.Tests.csproj.DotSettings 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 From 6848c910e13ef4f819b1d8c5fbbcfb74af004554 Mon Sep 17 00:00:00 2001 From: Sergey Odinokov Date: Wed, 8 Apr 2020 11:11:03 +0300 Subject: [PATCH 12/19] Use new lang features for recently touched context classes --- src/Hangfire.Core/Server/PerformContext.cs | 13 ++++------- src/Hangfire.Core/States/ApplyStateContext.cs | 22 ++++++------------- src/Hangfire.Core/States/ElectStateContext.cs | 4 ++-- .../States/StateChangeContext.cs | 16 +++++--------- 4 files changed, 18 insertions(+), 37 deletions(-) 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/States/ApplyStateContext.cs b/src/Hangfire.Core/States/ApplyStateContext.cs index 7ab66ac3b..13f2eb800 100644 --- a/src/Hangfire.Core/States/ApplyStateContext.cs +++ b/src/Hangfire.Core/States/ApplyStateContext.cs @@ -55,23 +55,15 @@ internal ApplyStateContext( [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)); - if (profiler == null) throw new ArgumentNullException(nameof(profiler)); - - 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; - JobExpirationTimeout = storage.JobExpirationTimeout; - Profiler = profiler; + Profiler = profiler ?? throw new ArgumentNullException(nameof(profiler)); CustomData = customData; + JobExpirationTimeout = storage.JobExpirationTimeout; } [NotNull] diff --git a/src/Hangfire.Core/States/ElectStateContext.cs b/src/Hangfire.Core/States/ElectStateContext.cs index 1d0684f8e..15fdec63b 100644 --- a/src/Hangfire.Core/States/ElectStateContext.cs +++ b/src/Hangfire.Core/States/ElectStateContext.cs @@ -60,7 +60,7 @@ public ElectStateContext([NotNull] ApplyStateContext applyContext) [NotNull] public IState CandidateState { - get { return _candidateState; } + get => _candidateState; set { if (value == null) @@ -77,7 +77,7 @@ public IState CandidateState } [CanBeNull] - public string CurrentState { get; private set; } + public string CurrentState { get; } [NotNull] public IState[] TraversedStates => _traversedStates.ToArray(); diff --git a/src/Hangfire.Core/States/StateChangeContext.cs b/src/Hangfire.Core/States/StateChangeContext.cs index 7526ba8ed..9152ee260 100644 --- a/src/Hangfire.Core/States/StateChangeContext.cs +++ b/src/Hangfire.Core/States/StateChangeContext.cs @@ -65,19 +65,13 @@ internal StateChangeContext( [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; CancellationToken = cancellationToken; - Profiler = profiler; + Profiler = profiler ?? throw new ArgumentNullException(nameof(profiler)); CustomData = customData; } From 03a7f5bf954dfbb02daeb943377332ce831c6ee2 Mon Sep 17 00:00:00 2001 From: William Buchanan Date: Fri, 15 May 2020 15:22:30 +1000 Subject: [PATCH 13/19] Add suspend and resume functionality --- .../Hangfire.AspNetCore.csproj | 6 +- .../Content/resx/Strings.Designer.cs | 58 +- .../Dashboard/Content/resx/Strings.resx | 24 + .../Dashboard/DashboardMetrics.cs | 23 +- .../Dashboard/DashboardRoutes.cs | 10 + .../Dashboard/JobsSidebarMenu.cs | 6 + .../Dashboard/Pages/SuspendedJobsPage.cshtml | 171 +++++ .../Pages/SuspendedJobsPage.cshtml.cs | 648 ++++++++++++++++++ src/Hangfire.Core/Hangfire.Core.csproj | 13 +- src/Hangfire.Core/IRecurringJobManager.cs | 12 + src/Hangfire.Core/RecurringJobManager.cs | 49 +- .../Hangfire.SqlServer.Msmq.csproj | 4 +- .../Hangfire.SqlServer.csproj | 8 +- 13 files changed, 1013 insertions(+), 19 deletions(-) create mode 100644 src/Hangfire.Core/Dashboard/Pages/SuspendedJobsPage.cshtml create mode 100644 src/Hangfire.Core/Dashboard/Pages/SuspendedJobsPage.cshtml.cs 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/Dashboard/Content/resx/Strings.Designer.cs b/src/Hangfire.Core/Dashboard/Content/resx/Strings.Designer.cs index a34d66f2b..eaac10f4e 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; @@ -684,6 +682,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. /// @@ -828,6 +835,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. /// @@ -1305,5 +1321,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 55edada52..139d224f7 100644 --- a/src/Hangfire.Core/Dashboard/Content/resx/Strings.resx +++ b/src/Hangfire.Core/Dashboard/Content/resx/Strings.resx @@ -545,4 +545,28 @@ 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 + \ No newline at end of file diff --git a/src/Hangfire.Core/Dashboard/DashboardMetrics.cs b/src/Hangfire.Core/Dashboard/DashboardMetrics.cs index 5d5c29a5f..3ce256be8 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) { @@ -190,5 +190,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/SuspendedJobsPage.cshtml b/src/Hangfire.Core/Dashboard/Pages/SuspendedJobsPage.cshtml new file mode 100644 index 000000000..a5f729ced --- /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() +
+
+

xxxx @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 9ef9bd255..bda7c7c35 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..26aed8c0f 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 { @@ -193,5 +194,51 @@ 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 (jd != null && jd != "") + recurringJobId = String.Format(jd).ToString().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.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 @@ - + - + From fc8d8796afc31a4d4ed95dc9f05dd01909798139 Mon Sep 17 00:00:00 2001 From: William Buchanan Date: Fri, 15 May 2020 15:25:56 +1000 Subject: [PATCH 14/19] Fix debug code --- src/Hangfire.Core/Dashboard/Pages/SuspendedJobsPage.cshtml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Hangfire.Core/Dashboard/Pages/SuspendedJobsPage.cshtml b/src/Hangfire.Core/Dashboard/Pages/SuspendedJobsPage.cshtml index a5f729ced..9d5633f91 100644 --- a/src/Hangfire.Core/Dashboard/Pages/SuspendedJobsPage.cshtml +++ b/src/Hangfire.Core/Dashboard/Pages/SuspendedJobsPage.cshtml @@ -9,7 +9,7 @@ @using Hangfire.Storage @inherits RazorPage @{ - Layout = new LayoutPage("Strings.SuspendedJobsPage_Title"); + Layout = new LayoutPage(Strings.SuspendedJobsPage_Title); int from, perPage; @@ -44,7 +44,7 @@ else @Html.JobsSidebar()
-

xxxx @Strings.SuspendedJobsPage_Title

+

@Strings.SuspendedJobsPage_Title

@if (jobIds.Count == 0) {
From be14ca25fad243a013e72db1041b27715b2e6670 Mon Sep 17 00:00:00 2001 From: William Buchanan Date: Mon, 18 May 2020 11:31:55 +1000 Subject: [PATCH 15/19] Fix minor bug with string format --- src/Hangfire.Core/RecurringJobManager.cs | 31 ++++++++++++++---------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/Hangfire.Core/RecurringJobManager.cs b/src/Hangfire.Core/RecurringJobManager.cs index 26aed8c0f..2f7fee3db 100644 --- a/src/Hangfire.Core/RecurringJobManager.cs +++ b/src/Hangfire.Core/RecurringJobManager.cs @@ -55,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) @@ -63,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) @@ -82,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) @@ -122,7 +122,7 @@ public void AddOrUpdate(string recurringJobId, Job job, string cronExpression, R } } } - + private static void ValidateCronExpression(string cronExpression) { try @@ -215,7 +215,9 @@ public void SuspendJob(string recurringJobId) public void ResumeJob(string recurringJobId) { - if (recurringJobId == null) throw new ArgumentNullException(nameof(recurringJobId)); + if (recurringJobId == null) + throw new ArgumentNullException(nameof(recurringJobId)); + var monitor = _storage.GetMonitoringApi(); var job = monitor.JobDetails(recurringJobId); string jd = ""; @@ -226,17 +228,20 @@ public void ResumeJob(string recurringJobId) jd = RecurringJobId.Value; } } - if (jd != null && jd != "") - recurringJobId = String.Format(jd).ToString().Replace("\"", ""); + + if (!string.IsNullOrEmpty(jd)) + recurringJobId = jd.Replace("\"", ""); using (var connection = _storage.GetConnection()) - using (connection.AcquireDistributedRecurringJobLock(recurringJobId, DefaultTimeout)) { - using (var transaction = connection.CreateWriteTransaction()) + using (connection.AcquireDistributedRecurringJobLock(recurringJobId, DefaultTimeout)) { - transaction.RemoveFromSet("paused-jobs", recurringJobId); - transaction.AddToSet("recurring-jobs", recurringJobId, JobHelper.ToTimestamp(DateTime.UtcNow)); - transaction.Commit(); + using (var transaction = connection.CreateWriteTransaction()) + { + transaction.RemoveFromSet("paused-jobs", recurringJobId); + transaction.AddToSet("recurring-jobs", recurringJobId, JobHelper.ToTimestamp(DateTime.UtcNow)); + transaction.Commit(); + } } } } From cedc8b325224ec9cc1ef488dc573f20279f8e396 Mon Sep 17 00:00:00 2001 From: Sergey Odinokov Date: Mon, 12 Oct 2020 16:34:43 +0300 Subject: [PATCH 16/19] Update StateChangeContextFacts.cs --- tests/Hangfire.Core.Tests/States/StateChangeContextFacts.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/Hangfire.Core.Tests/States/StateChangeContextFacts.cs b/tests/Hangfire.Core.Tests/States/StateChangeContextFacts.cs index b7c03c892..19ddd9aed 100644 --- a/tests/Hangfire.Core.Tests/States/StateChangeContextFacts.cs +++ b/tests/Hangfire.Core.Tests/States/StateChangeContextFacts.cs @@ -96,6 +96,7 @@ public void Ctor_ThrowsAnException_WhenProfilerIsNull() JobId, _newState.Object, null, + false, CancellationToken.None, null)); @@ -171,6 +172,7 @@ public void InternalCtor_CorrectlySets_AllTheProperties() JobId, _newState.Object, _expectedStates, + false, _token, _profiler.Object, _customData); @@ -180,6 +182,7 @@ public void InternalCtor_CorrectlySets_AllTheProperties() 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); From e56c88f73ae802d23efb5ca117089c5eebc54040 Mon Sep 17 00:00:00 2001 From: Sergey Odinokov Date: Wed, 20 Jan 2021 15:34:54 +0300 Subject: [PATCH 17/19] Ignore some members when serializing JobFilterAttribute to decrease size --- .../Common/JobFilterAttribute.cs | 10 +++++ .../Common/JobFilterAttributeFacts.cs | 45 ++++++++++++++++++- 2 files changed, 54 insertions(+), 1 deletion(-) 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/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 + { + } } } From 9d691b49debe7b324013bfb7dd9a9519f9da9d6f Mon Sep 17 00:00:00 2001 From: Sergey Odinokov Date: Wed, 20 Jan 2021 16:05:13 +0300 Subject: [PATCH 18/19] Fetch "Retries" metric with other statistics when supported by storage --- .../Dashboard/DashboardMetrics.cs | 20 +++++++++++++------ .../Storage/Monitoring/StatisticsDto.cs | 1 + .../SqlServerMonitoringApi.cs | 2 ++ 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/Hangfire.Core/Dashboard/DashboardMetrics.cs b/src/Hangfire.Core/Dashboard/DashboardMetrics.cs index 5d5c29a5f..1e94b4c95 100644 --- a/src/Hangfire.Core/Dashboard/DashboardMetrics.cs +++ b/src/Hangfire.Core/Dashboard/DashboardMetrics.cs @@ -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) 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/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; }); From fc6787099df7d6f5899d0c6a0c596115323f0c3d Mon Sep 17 00:00:00 2001 From: Sergey Odinokov Date: Wed, 20 Jan 2021 16:34:13 +0300 Subject: [PATCH 19/19] Display deleted jobs in the realtime graph --- .../Dashboard/Content/js/hangfire.js | 18 +++++--- .../Dashboard/Pages/HomePage.cshtml | 6 ++- .../Dashboard/Pages/HomePage.cshtml.cs | 45 ++++++++++++++----- 3 files changed, 50 insertions(+), 19 deletions(-) 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/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" }