├── Hangfire_Redis_StackExchange.snk
├── Hangfire.Redis.Tests
├── Hangfire_Redis_Tests.snk
├── xunit.runner.json
├── Hangfire.Redis.Tests.csproj.DotSettings
├── Utils
│ ├── StaticFakeJobs.cs
│ ├── CleanRedisAttribute.cs
│ └── RedisUtils.cs
├── RedisTest.cs
├── RedisStorageOptionsFacts.cs
├── Hangfire.Redis.Tests.csproj
├── ApplyStateContextMock.cs
├── ProcessingStateHandlerFacts.cs
├── DeletedStateHandlerFacts.cs
├── FailedStateHandlerFacts.cs
├── SucceededStateHandlerFacts.cs
├── RedisSubscriptionFacts.cs
├── RedisStorageFacts.cs
├── ExpiredJobsWatcherFacts.cs
├── RedisLockFacts.cs
├── FetchedJobsWatcherFacts.cs
├── RedisConnectionFacts.cs
├── RedisFetchedJobFacts.cs
└── RedisWriteOnlyTransactionFacts.cs
├── Hangfire.Redis.StackExchange
├── FetchedJobsWatcher.cs
├── FetchedJobsWatcherOptions.cs
├── Hangfire_Redis_StackExchange.snk
├── Properties
│ └── AssemblyInfo.cs
├── Hangfire.Redis.StackExchange.csproj.DotSettings
├── HangfireSubscriber.cs
├── HangFireRedisException.cs
├── HangfireRedisTransactionException.cs
├── FailedStateHandler.cs
├── ProcessingStateHandler.cs
├── RedisSubscription.cs
├── SucceededStateHandler.cs
├── DeletedStateHandler.cs
├── RedisDatabaseExtensions.cs
├── RedisStorageExtensions.cs
├── RedisStorageOptions.cs
├── ExpiredJobsWatcher.cs
├── RedisFetchedJob.cs
├── RedisInfoKeys.cs
├── Hangfire.Redis.StackExchange.csproj
├── RedisLock.cs
├── RedisStorage.cs
├── RedisWriteDirectlyToDatabase.cs
├── RedisWriteOnlyTransaction.cs
└── RedisConnection.cs
├── Hangfire.Redis.StackExchange.sln.DotSettings
├── License.md
├── .travis.yml
├── appveyor.yml
├── Hangfire.Redis.StackExchange.sln
├── .gitattributes
├── .gitignore
└── README.md
/Hangfire_Redis_StackExchange.snk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcoCasamento/Hangfire.Redis.StackExchange/HEAD/Hangfire_Redis_StackExchange.snk
--------------------------------------------------------------------------------
/Hangfire.Redis.Tests/Hangfire_Redis_Tests.snk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcoCasamento/Hangfire.Redis.StackExchange/HEAD/Hangfire.Redis.Tests/Hangfire_Redis_Tests.snk
--------------------------------------------------------------------------------
/Hangfire.Redis.StackExchange/FetchedJobsWatcher.cs:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcoCasamento/Hangfire.Redis.StackExchange/HEAD/Hangfire.Redis.StackExchange/FetchedJobsWatcher.cs
--------------------------------------------------------------------------------
/Hangfire.Redis.StackExchange/FetchedJobsWatcherOptions.cs:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcoCasamento/Hangfire.Redis.StackExchange/HEAD/Hangfire.Redis.StackExchange/FetchedJobsWatcherOptions.cs
--------------------------------------------------------------------------------
/Hangfire.Redis.StackExchange/Hangfire_Redis_StackExchange.snk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcoCasamento/Hangfire.Redis.StackExchange/HEAD/Hangfire.Redis.StackExchange/Hangfire_Redis_StackExchange.snk
--------------------------------------------------------------------------------
/Hangfire.Redis.Tests/xunit.runner.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://raw.githubusercontent.com/SchemaStore/schemastore/master/src/schemas/json/xunit.runner.schema.json",
3 | "parallelizeTestCollections": false
4 | }
--------------------------------------------------------------------------------
/Hangfire.Redis.StackExchange.sln.DotSettings:
--------------------------------------------------------------------------------
1 |
2 | True
--------------------------------------------------------------------------------
/License.md:
--------------------------------------------------------------------------------
1 | License
2 | ========
3 |
4 | Copyright © 2015 Marco Casamento.
5 | Original Version of the product by Sergey Odinokov available at https://github.com/HangfireIO/Hangfire.Redis/
6 |
7 | Hangfire.Redis.StackExchange is licensed under the terms of the LGPLv3 license, that is available at http://www.gnu.org/licenses/lgpl-3.0.html.
8 | It also makes use of https://github.com/StackExchange/StackExchange.Redis licensed under his own terms (MIT)
9 |
--------------------------------------------------------------------------------
/Hangfire.Redis.Tests/Hangfire.Redis.Tests.csproj.DotSettings:
--------------------------------------------------------------------------------
1 |
2 | Preview
--------------------------------------------------------------------------------
/Hangfire.Redis.StackExchange/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.CompilerServices;
2 |
3 | // General Information about an assembly is controlled through the following
4 | // set of attributes. Change these attribute values to modify the information
5 | // associated with an assembly.
6 | [assembly: InternalsVisibleTo("Hangfire.Redis.Tests")]
7 |
8 | // Allow the generation of mocks for internal types
9 | //[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
--------------------------------------------------------------------------------
/Hangfire.Redis.StackExchange/Hangfire.Redis.StackExchange.csproj.DotSettings:
--------------------------------------------------------------------------------
1 |
2 | Latest
--------------------------------------------------------------------------------
/Hangfire.Redis.Tests/Utils/StaticFakeJobs.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 |
4 | namespace Hangfire.Redis.Tests.Utils
5 | {
6 | public static class StaticFakeJobs
7 | {
8 | public static string Work(int identifier, int waitTime)
9 | {
10 | Thread.Sleep(waitTime);
11 | var jobResult = String.Format("{0} - {1} Job done after waiting {2} ms", DateTime.Now.ToString("hh:mm:ss fff"), identifier, waitTime);
12 | Console.WriteLine(jobResult);
13 | return jobResult;
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Hangfire.Redis.Tests/Utils/CleanRedisAttribute.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using Xunit.Sdk;
3 |
4 | namespace Hangfire.Redis.Tests.Utils
5 | {
6 | public class CleanRedisAttribute : BeforeAfterTestAttribute
7 | {
8 | public override void Before(MethodInfo methodUnderTest)
9 | {
10 | var client = RedisUtils.CreateClient();
11 | client.Multiplexer.GetServer(RedisUtils.GetHostAndPort()).FlushDatabase(RedisUtils.GetDb());
12 | }
13 |
14 | public override void After(MethodInfo methodUnderTest)
15 | {
16 | }
17 | }
18 | }
--------------------------------------------------------------------------------
/Hangfire.Redis.Tests/RedisTest.cs:
--------------------------------------------------------------------------------
1 | using Hangfire.Redis.Tests.Utils;
2 | using StackExchange.Redis;
3 | using Xunit;
4 |
5 | namespace Hangfire.Redis.Tests
6 | {
7 | [Collection("Sequential")]
8 | public class RedisTest
9 | {
10 | private readonly IDatabase _redis;
11 |
12 | public RedisTest()
13 | {
14 | _redis = RedisUtils.CreateClient();
15 | }
16 |
17 |
18 | [Fact, CleanRedis]
19 | public void RedisSampleTest()
20 | {
21 | var defaultValue = _redis.StringGet("samplekey");
22 | Assert.True(defaultValue.IsNull);
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/Hangfire.Redis.Tests/RedisStorageOptionsFacts.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Hangfire.Redis.StackExchange;
3 | using Xunit;
4 |
5 | namespace Hangfire.Redis.Tests
6 | {
7 | [Collection("Sequential")]
8 | public class RedisStorageOptionsFacts
9 | {
10 |
11 | [Fact]
12 | public void InvisibilityTimeout_HasDefaultValue()
13 | {
14 | var options = CreateOptions();
15 | Assert.Equal(TimeSpan.FromMinutes(30), options.InvisibilityTimeout);
16 | }
17 |
18 | private static RedisStorageOptions CreateOptions()
19 | {
20 | return new RedisStorageOptions();
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | services:
3 | - redis-server
4 | language: csharp
5 | solution: Hangfire.Redis.StackExchange.sln
6 | install:
7 | - nuget restore Hangfire.Redis.StackExchange.sln -Verbosity detailed
8 | - nuget install xunit.runner.console -OutputDirectory testrunner
9 | script:
10 | - xbuild /p:Configuration=Release Hangfire.Redis.StackExchange.sln
11 | - cp -av ./testrunner/xunit.runner.console.2.0.0/tools/* ./Hangfire.Redis.Tests/bin/Release/
12 | - cp ./StackExchange.Redis/StackExchange.Redis/bin/Release/StackExchange.Redis.dll ./Hangfire.Redis.Tests/bin/Release/
13 | - mono ./Hangfire.Redis.Tests/bin/Release/xunit.console.x86.exe ./Hangfire.Redis.Tests/bin/Release/Hangfire.Redis.Tests.dll -parallel none -maxthreads 1
14 |
--------------------------------------------------------------------------------
/Hangfire.Redis.StackExchange/HangfireSubscriber.cs:
--------------------------------------------------------------------------------
1 | using Hangfire.Server;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Text;
5 | using System.Threading;
6 |
7 | namespace Hangfire.Redis.StackExchange
8 | {
9 | ///
10 | /// Singelton used to keep track of hangfire jobs
11 | ///
12 | /// Came from https://github.com/AnderssonPeter/Hangfire.Console.Extensions/blob/master/Hangfire.Console.Extensions/HangfireSubscriber.cs
13 | internal class HangfireSubscriber : IServerFilter
14 | {
15 | private static readonly AsyncLocal localStorage = new AsyncLocal();
16 |
17 | public static PerformingContext Value => localStorage.Value;
18 |
19 | public void OnPerforming(PerformingContext filterContext)
20 | {
21 | localStorage.Value = filterContext;
22 | }
23 |
24 | public void OnPerformed(PerformedContext filterContext)
25 | {
26 | localStorage.Value = null;
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Hangfire.Redis.StackExchange/HangFireRedisException.cs:
--------------------------------------------------------------------------------
1 | // Copyright © 2013-2015 Sergey Odinokov, Marco Casamento
2 | // This software is based on https://github.com/HangfireIO/Hangfire.Redis
3 |
4 | // Hangfire.Redis.StackExchange is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as
6 | // published by the Free Software Foundation, either version 3
7 | // of the License, or any later version.
8 | //
9 | // Hangfire.Redis.StackExchange is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public
15 | // License along with Hangfire.Redis.StackExchange. If not, see .
16 |
17 | using System;
18 |
19 | namespace Hangfire.Redis.StackExchange
20 | {
21 | public class HangFireRedisException : Exception
22 | {
23 | public HangFireRedisException(string message) : base(message)
24 | {
25 | }
26 | }
27 | }
--------------------------------------------------------------------------------
/Hangfire.Redis.StackExchange/HangfireRedisTransactionException.cs:
--------------------------------------------------------------------------------
1 | // Copyright © 2013-2015 Sergey Odinokov, Marco Casamento
2 | // This software is based on https://github.com/HangfireIO/Hangfire.Redis
3 |
4 | // Hangfire.Redis.StackExchange is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as
6 | // published by the Free Software Foundation, either version 3
7 | // of the License, or any later version.
8 | //
9 | // Hangfire.Redis.StackExchange is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public
15 | // License along with Hangfire.Redis.StackExchange. If not, see .
16 |
17 | using System;
18 |
19 | namespace Hangfire.Redis.StackExchange
20 | {
21 | public class HangfireRedisTransactionException : Exception
22 | {
23 | public HangfireRedisTransactionException(string message) : base(message)
24 | {
25 | }
26 | }
27 | }
--------------------------------------------------------------------------------
/appveyor.yml:
--------------------------------------------------------------------------------
1 | version: 1.9.0.{build}
2 | branches:
3 | only:
4 | - master
5 | image: Visual Studio 2022
6 | configuration: Release
7 | platform: Any CPU
8 |
9 | install:
10 | - cmd: nuget install redis-64 -excludeversion
11 | - cmd: redis-64\tools\redis-server.exe --service-install
12 | - cmd: redis-64\tools\redis-server.exe --service-start
13 | before_build:
14 | - dotnet restore
15 | build:
16 | project: Hangfire.Redis.StackExchange.sln
17 | publish_nuget: true
18 | publish_nuget_symbols: true
19 | verbosity: minimal
20 | artifacts:
21 | - path: .\**\Hangfire.Redis.StackExchange.nupkg
22 | assembly_info:
23 | patch: true
24 | file: '**\AssemblyInfo.*'
25 | assembly_version: '{version}'
26 | assembly_file_version: '{version}'
27 | assembly_informational_version: '{version}'
28 | nuget:
29 | account_feed: false
30 | project_feed: false
31 | test_script:
32 | - dotnet test ".\Hangfire.Redis.Tests\Hangfire.Redis.Tests.csproj"
33 | #before_package:
34 | #- ps: nuget pack .\Hangfire.Redis.StackExchange\HangFire.Redis.StackExchange.nuspec -Version $env:APPVEYOR_BUILD_VERSION
35 | #deploy:
36 | # provider: NuGet
37 | # api_key:
38 | # secure: hiLTKk/ItMBgRI5HCemg6kARhcAGs6/V/eQpymZAdwQOtHgD8waLBtb3VMdgPRYG
39 | # artifact: /.*\.nupkg/
40 |
--------------------------------------------------------------------------------
/Hangfire.Redis.Tests/Hangfire.Redis.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net9.0
5 | false
6 | Hangfire_Redis_Tests.snk
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | all
16 | runtime; build; native; contentfiles; analyzers; buildtransitive
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | disable
30 |
31 |
32 |
33 |
34 | PreserveNewest
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/Hangfire.Redis.Tests/ApplyStateContextMock.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Hangfire.States;
3 | using Moq;
4 | using Hangfire.Common;
5 | using Hangfire.Redis.StackExchange;
6 | using Hangfire.Redis.Tests.Utils;
7 |
8 | namespace Hangfire.Redis.Tests
9 | {
10 | public class ApplyStateContextMock
11 | {
12 | private readonly Lazy _context;
13 |
14 | public ApplyStateContextMock(string jobId)
15 | {
16 | NewStateValue = new Mock().Object;
17 | OldStateValue = null;
18 | var storage = CreateStorage();
19 | var connection = storage.GetConnection();
20 | var writeOnlyTransaction = connection.CreateWriteTransaction();
21 |
22 | var job = new Job(this.GetType().GetMethod("GetType"));
23 | var backgroundJob = new BackgroundJob(jobId, job, DateTime.MinValue);
24 |
25 | _context = new Lazy(
26 | () => new ApplyStateContext(
27 | storage, connection, writeOnlyTransaction, backgroundJob,
28 | NewStateValue,
29 | OldStateValue));
30 | }
31 |
32 | public IState NewStateValue { get; set; }
33 | public string OldStateValue { get; set; }
34 |
35 | public ApplyStateContext Object => _context.Value;
36 |
37 | private RedisStorage CreateStorage()
38 | {
39 | var options = new RedisStorageOptions() { Db = RedisUtils.GetDb() };
40 | return new RedisStorage(RedisUtils.GetHostAndPort(), options);
41 | }
42 |
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Hangfire.Redis.Tests/ProcessingStateHandlerFacts.cs:
--------------------------------------------------------------------------------
1 | using Hangfire.Redis.StackExchange;
2 | using Hangfire.Redis.Tests.Utils;
3 | using Hangfire.States;
4 | using Hangfire.Storage;
5 | using Moq;
6 | using Xunit;
7 |
8 | namespace Hangfire.Redis.Tests
9 | {
10 | [CleanRedis, Collection("Sequential")]
11 | public class ProcessingStateHandlerFacts
12 | {
13 | private const string JobId = "1";
14 |
15 | private readonly ApplyStateContextMock _context;
16 | private readonly Mock _transaction;
17 |
18 | public ProcessingStateHandlerFacts()
19 | {
20 | _context = new ApplyStateContextMock(JobId);
21 | _transaction = new Mock();
22 | }
23 |
24 | [Fact]
25 | public void StateName_ShouldBeEqualToProcessingState()
26 | {
27 | var handler = new ProcessingStateHandler();
28 | Assert.Equal(ProcessingState.StateName, handler.StateName);
29 | }
30 |
31 | [Fact]
32 | public void Apply_ShouldAddTheJob_ToTheProcessingSet()
33 | {
34 | var handler = new ProcessingStateHandler();
35 | handler.Apply(_context.Object, _transaction.Object);
36 |
37 | _transaction.Verify(x => x.AddToSet(
38 | "processing", JobId, It.IsAny()));
39 | }
40 |
41 | [Fact]
42 | public void Unapply_ShouldRemoveTheJob_FromTheProcessingSet()
43 | {
44 | var handler = new ProcessingStateHandler();
45 | handler.Unapply(_context.Object, _transaction.Object);
46 |
47 | _transaction.Verify(x => x.RemoveFromSet("processing", JobId));
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Hangfire.Redis.StackExchange/FailedStateHandler.cs:
--------------------------------------------------------------------------------
1 | // Copyright © 2013-2015 Sergey Odinokov, Marco Casamento
2 | // This software is based on https://github.com/HangfireIO/Hangfire.Redis
3 |
4 | // Hangfire.Redis.StackExchange is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as
6 | // published by the Free Software Foundation, either version 3
7 | // of the License, or any later version.
8 | //
9 | // Hangfire.Redis.StackExchange is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public
15 | // License along with Hangfire.Redis.StackExchange. If not, see .
16 |
17 | using System;
18 | using Hangfire.Common;
19 | using Hangfire.States;
20 | using Hangfire.Storage;
21 |
22 | namespace Hangfire.Redis.StackExchange
23 | {
24 | internal class FailedStateHandler : IStateHandler
25 | {
26 | public void Apply(ApplyStateContext context, IWriteOnlyTransaction transaction)
27 | {
28 | transaction.AddToSet(
29 | "failed",
30 | context.BackgroundJob.Id,
31 | JobHelper.ToTimestamp(DateTime.UtcNow));
32 | }
33 |
34 | public void Unapply(ApplyStateContext context, IWriteOnlyTransaction transaction)
35 | {
36 | transaction.RemoveFromSet("failed", context.BackgroundJob.Id);
37 | }
38 |
39 | public string StateName
40 | {
41 | get { return FailedState.StateName; }
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Hangfire.Redis.StackExchange/ProcessingStateHandler.cs:
--------------------------------------------------------------------------------
1 | // Copyright © 2013-2015 Sergey Odinokov, Marco Casamento
2 | // This software is based on https://github.com/HangfireIO/Hangfire.Redis
3 |
4 | // Hangfire.Redis.StackExchange is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as
6 | // published by the Free Software Foundation, either version 3
7 | // of the License, or any later version.
8 | //
9 | // Hangfire.Redis.StackExchange is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public
15 | // License along with Hangfire.Redis.StackExchange. If not, see .
16 |
17 | using System;
18 | using Hangfire.Common;
19 | using Hangfire.States;
20 | using Hangfire.Storage;
21 |
22 | namespace Hangfire.Redis.StackExchange
23 | {
24 | internal class ProcessingStateHandler : IStateHandler
25 | {
26 | public void Apply(ApplyStateContext context, IWriteOnlyTransaction transaction)
27 | {
28 | transaction.AddToSet(
29 | "processing",
30 | context.BackgroundJob.Id,
31 | JobHelper.ToTimestamp(DateTime.UtcNow));
32 | }
33 |
34 | public void Unapply(ApplyStateContext context, IWriteOnlyTransaction transaction)
35 | {
36 | transaction.RemoveFromSet("processing", context.BackgroundJob.Id);
37 | }
38 |
39 | public string StateName
40 | {
41 | get { return ProcessingState.StateName; }
42 | }
43 | }
44 | }
--------------------------------------------------------------------------------
/Hangfire.Redis.Tests/DeletedStateHandlerFacts.cs:
--------------------------------------------------------------------------------
1 | using Hangfire.Redis.StackExchange;
2 | using Hangfire.Redis.Tests.Utils;
3 | using Hangfire.States;
4 | using Hangfire.Storage;
5 | using Moq;
6 | using Xunit;
7 |
8 | namespace Hangfire.Redis.Tests
9 | {
10 | [CleanRedis, Collection("Sequential")]
11 | public class DeletedStateHandlerFacts
12 | {
13 | private const string JobId = "1";
14 |
15 | private readonly ApplyStateContextMock _context;
16 | private readonly Mock _transaction;
17 |
18 | public DeletedStateHandlerFacts()
19 | {
20 | _context = new ApplyStateContextMock(JobId);
21 | _transaction = new Mock();
22 | }
23 |
24 | [Fact]
25 | public void StateName_ShouldBeEqualToSucceededState()
26 | {
27 | var handler = new DeletedStateHandler();
28 | Assert.Equal(DeletedState.StateName, handler.StateName);
29 | }
30 |
31 | [Fact]
32 | public void Apply_ShouldInsertTheJob_ToTheBeginningOfTheSucceededList_AndTrimIt()
33 | {
34 | var handler = new DeletedStateHandler();
35 | handler.Apply(_context.Object, _transaction.Object);
36 |
37 | _transaction.Verify(x => x.InsertToList(
38 | "deleted", JobId));
39 | _transaction.Verify(x => x.TrimList(
40 | "deleted", 0, 499));
41 | }
42 |
43 | [Fact]
44 | public void Unapply_ShouldRemoveTheJob_FromTheSucceededList()
45 | {
46 | var handler = new DeletedStateHandler();
47 | handler.Unapply(_context.Object, _transaction.Object);
48 |
49 | _transaction.Verify(x => x.RemoveFromList("deleted", JobId));
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Hangfire.Redis.Tests/FailedStateHandlerFacts.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Hangfire.Redis.StackExchange;
3 | using Hangfire.Redis.Tests.Utils;
4 | using Hangfire.States;
5 | using Hangfire.Storage;
6 | using Moq;
7 | using Xunit;
8 |
9 | namespace Hangfire.Redis.Tests
10 | {
11 | [CleanRedis, Collection("Sequential")]
12 | public class FailedStateHandlerFacts
13 | {
14 | private const string JobId = "1";
15 |
16 | private readonly ApplyStateContextMock _context;
17 | private readonly Mock _transaction;
18 |
19 | public FailedStateHandlerFacts()
20 | {
21 | _context = new ApplyStateContextMock(JobId)
22 | {
23 | NewStateValue = new FailedState(new InvalidOperationException())
24 | };
25 |
26 | _transaction = new Mock();
27 | }
28 |
29 | [Fact]
30 | public void StateName_ShouldBeEqualToFailedState()
31 | {
32 | var handler = new FailedStateHandler();
33 | Assert.Equal(FailedState.StateName, handler.StateName);
34 | }
35 |
36 | [Fact]
37 | public void Apply_ShouldAddTheJob_ToTheFailedSet()
38 | {
39 | var handler = new FailedStateHandler();
40 | handler.Apply(_context.Object, _transaction.Object);
41 |
42 | _transaction.Verify(x => x.AddToSet(
43 | "failed", JobId, It.IsAny()));
44 | }
45 |
46 | [Fact]
47 | public void Unapply_ShouldRemoveTheJob_FromTheFailedSet()
48 | {
49 | var handler = new FailedStateHandler();
50 | handler.Unapply(_context.Object, _transaction.Object);
51 |
52 | _transaction.Verify(x => x.RemoveFromSet("failed", JobId));
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/Hangfire.Redis.Tests/SucceededStateHandlerFacts.cs:
--------------------------------------------------------------------------------
1 | using Hangfire.Redis.StackExchange;
2 | using Hangfire.Redis.Tests.Utils;
3 | using Hangfire.States;
4 | using Hangfire.Storage;
5 | using Moq;
6 | using Xunit;
7 |
8 | namespace Hangfire.Redis.Tests
9 | {
10 | [CleanRedis, Collection("Sequential")]
11 | public class SucceededStateHandlerFacts
12 | {
13 | private const string JobId = "1";
14 |
15 | private readonly ApplyStateContextMock _context;
16 | private readonly Mock _transaction;
17 |
18 | public SucceededStateHandlerFacts()
19 | {
20 | _context = new ApplyStateContextMock(JobId);
21 | _transaction = new Mock();
22 | }
23 |
24 | [Fact]
25 | public void StateName_ShouldBeEqualToSucceededState()
26 | {
27 | var handler = new SucceededStateHandler();
28 | Assert.Equal(SucceededState.StateName, handler.StateName);
29 | }
30 |
31 | [Fact]
32 | public void Apply_ShouldInsertTheJob_ToTheBeginningOfTheSucceededList_AndTrimIt()
33 | {
34 | var handler = new SucceededStateHandler();
35 | handler.Apply(_context.Object, _transaction.Object);
36 |
37 | _transaction.Verify(x => x.InsertToList(
38 | "succeeded", JobId));
39 | _transaction.Verify(x => x.TrimList(
40 | "succeeded", 0, 499));
41 | }
42 |
43 | [Fact]
44 | public void Unapply_ShouldRemoveTheJob_FromTheSucceededList()
45 | {
46 | var handler = new SucceededStateHandler();
47 | handler.Unapply(_context.Object, _transaction.Object);
48 |
49 | _transaction.Verify(x => x.RemoveFromList("succeeded", JobId));
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Hangfire.Redis.StackExchange/RedisSubscription.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 | using Hangfire.Annotations;
4 | using Hangfire.Server;
5 | using StackExchange.Redis;
6 |
7 | namespace Hangfire.Redis.StackExchange
8 | {
9 | #pragma warning disable 618
10 | internal class RedisSubscription : IServerComponent
11 | #pragma warning restore 618
12 | {
13 | private readonly ManualResetEvent _mre = new ManualResetEvent(false);
14 | private readonly RedisStorage _storage;
15 | private readonly ISubscriber _subscriber;
16 |
17 | public RedisSubscription([NotNull] RedisStorage storage, [NotNull] ISubscriber subscriber)
18 | {
19 | _storage = storage ?? throw new ArgumentNullException(nameof(storage));
20 | Channel = new RedisChannel(_storage.GetRedisKey("JobFetchChannel"), RedisChannel.PatternMode.Literal);
21 | _subscriber = subscriber ?? throw new ArgumentNullException(nameof(subscriber));
22 |
23 | }
24 |
25 | public RedisChannel Channel { get; }
26 |
27 | public void WaitForJob(TimeSpan timeout, CancellationToken cancellationToken)
28 | {
29 | _mre.Reset();
30 | WaitHandle.WaitAny(new[] {_mre, cancellationToken.WaitHandle}, timeout);
31 | }
32 |
33 | void IServerComponent.Execute(CancellationToken cancellationToken)
34 | {
35 | _subscriber.Subscribe(Channel, (channel, value) => _mre.Set());
36 | cancellationToken.WaitHandle.WaitOne();
37 |
38 | if (cancellationToken.IsCancellationRequested)
39 | {
40 | _subscriber.Unsubscribe(Channel);
41 | _mre.Reset();
42 | }
43 | }
44 |
45 | ~RedisSubscription()
46 | {
47 | _mre.Dispose();
48 | }
49 | }
50 | }
--------------------------------------------------------------------------------
/Hangfire.Redis.StackExchange/SucceededStateHandler.cs:
--------------------------------------------------------------------------------
1 | // Copyright © 2013-2015 Sergey Odinokov, Marco Casamento
2 | // This software is based on https://github.com/HangfireIO/Hangfire.Redis
3 |
4 | // Hangfire.Redis.StackExchange is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as
6 | // published by the Free Software Foundation, either version 3
7 | // of the License, or any later version.
8 | //
9 | // Hangfire.Redis.StackExchange is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public
15 | // License along with Hangfire.Redis.StackExchange. If not, see .
16 |
17 | using Hangfire.States;
18 | using Hangfire.Storage;
19 |
20 | namespace Hangfire.Redis.StackExchange
21 | {
22 | internal class SucceededStateHandler : IStateHandler
23 | {
24 | public void Apply(ApplyStateContext context, IWriteOnlyTransaction transaction)
25 | {
26 | transaction.InsertToList("succeeded", context.BackgroundJob.Id);
27 |
28 | var storage = context.Storage as RedisStorage;
29 | if (storage != null && storage.SucceededListSize > 0)
30 | {
31 | transaction.TrimList("succeeded", 0, storage.SucceededListSize);
32 | }
33 | }
34 |
35 | public void Unapply(ApplyStateContext context, IWriteOnlyTransaction transaction)
36 | {
37 | transaction.RemoveFromList("succeeded", context.BackgroundJob.Id);
38 | }
39 |
40 | public string StateName => SucceededState.StateName;
41 | }
42 | }
--------------------------------------------------------------------------------
/Hangfire.Redis.StackExchange/DeletedStateHandler.cs:
--------------------------------------------------------------------------------
1 | // Copyright © 2013-2015 Sergey Odinokov, Marco Casamento
2 | // This software is based on https://github.com/HangfireIO/Hangfire.Redis
3 |
4 | // Hangfire.Redis.StackExchange is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as
6 | // published by the Free Software Foundation, either version 3
7 | // of the License, or any later version.
8 | //
9 | // Hangfire.Redis.StackExchange is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public
15 | // License along with Hangfire.Redis.StackExchange. If not, see .
16 |
17 | using Hangfire.States;
18 | using Hangfire.Storage;
19 |
20 | namespace Hangfire.Redis.StackExchange
21 | {
22 | internal class DeletedStateHandler : IStateHandler
23 | {
24 | public void Apply(ApplyStateContext context, IWriteOnlyTransaction transaction)
25 | {
26 | transaction.InsertToList("deleted", context.BackgroundJob.Id);
27 |
28 | var storage = context.Storage as RedisStorage;
29 | if (storage != null && storage.DeletedListSize > 0)
30 | {
31 | transaction.TrimList("deleted", 0, storage.DeletedListSize);
32 | }
33 | }
34 |
35 | public void Unapply(ApplyStateContext context, IWriteOnlyTransaction transaction)
36 | {
37 | transaction.RemoveFromList("deleted", context.BackgroundJob.Id);
38 | }
39 |
40 | public string StateName
41 | {
42 | get { return DeletedState.StateName; }
43 | }
44 | }
45 | }
--------------------------------------------------------------------------------
/Hangfire.Redis.Tests/RedisSubscriptionFacts.cs:
--------------------------------------------------------------------------------
1 | using Moq;
2 | using StackExchange.Redis;
3 | using System;
4 | using System.Diagnostics;
5 | using System.Threading;
6 | using Hangfire.Redis.StackExchange;
7 | using Hangfire.Redis.Tests.Utils;
8 | using Xunit;
9 |
10 | namespace Hangfire.Redis.Tests
11 | {
12 | [CleanRedis, Collection("Sequential")]
13 | public class RedisSubscriptionFacts
14 | {
15 | private readonly CancellationTokenSource _cts;
16 | private readonly RedisStorage _storage;
17 | private readonly Mock _subscriber;
18 |
19 | public RedisSubscriptionFacts()
20 | {
21 | _cts = new CancellationTokenSource();
22 |
23 | var options = new RedisStorageOptions() { Db = RedisUtils.GetDb() };
24 | _storage = new RedisStorage(RedisUtils.GetHostAndPort(), options);
25 |
26 | _subscriber = new Mock();
27 | }
28 |
29 | [Fact]
30 | public void Ctor_ThrowAnException_WhenStorageIsNull()
31 | {
32 | Assert.Throws("storage",
33 | () => new RedisSubscription(null, _subscriber.Object));
34 | }
35 |
36 | [Fact]
37 | public void Ctor_ThrowAnException_WhenSubscriberIsNull()
38 | {
39 | Assert.Throws("subscriber",
40 | () => new RedisSubscription(_storage, null));
41 | }
42 | [Fact]
43 | public void WaitForJob_WaitForTheTimeout()
44 | {
45 | //Arrange
46 | Stopwatch sw = new Stopwatch();
47 | var subscription = new RedisSubscription(_storage, RedisUtils.CreateSubscriber());
48 | var timeout = TimeSpan.FromMilliseconds(100);
49 | sw.Start();
50 |
51 | //Act
52 | subscription.WaitForJob(timeout, _cts.Token);
53 |
54 | //Assert
55 | sw.Stop();
56 | Assert.InRange(sw.ElapsedMilliseconds, 99, 120);
57 | }
58 | }
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/Hangfire.Redis.StackExchange.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.6.33417.168
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hangfire.Redis.StackExchange", "Hangfire.Redis.StackExchange\Hangfire.Redis.StackExchange.csproj", "{830696FE-FBDF-498C-AF5C-98B1E807AE0D}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hangfire.Redis.Tests", "Hangfire.Redis.Tests\Hangfire.Redis.Tests.csproj", "{E8031950-A2BD-48C1-966F-7B9E3DC37631}"
9 | EndProject
10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Project Items", "Project Items", "{EC2121D8-8CDD-4B1E-8BFF-57402A338C4F}"
11 | ProjectSection(SolutionItems) = preProject
12 | appveyor.yml = appveyor.yml
13 | License.md = License.md
14 | README.md = README.md
15 | EndProjectSection
16 | EndProject
17 | Global
18 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
19 | Debug|Any CPU = Debug|Any CPU
20 | Release|Any CPU = Release|Any CPU
21 | EndGlobalSection
22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
23 | {830696FE-FBDF-498C-AF5C-98B1E807AE0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
24 | {830696FE-FBDF-498C-AF5C-98B1E807AE0D}.Debug|Any CPU.Build.0 = Debug|Any CPU
25 | {830696FE-FBDF-498C-AF5C-98B1E807AE0D}.Release|Any CPU.ActiveCfg = Release|Any CPU
26 | {830696FE-FBDF-498C-AF5C-98B1E807AE0D}.Release|Any CPU.Build.0 = Release|Any CPU
27 | {E8031950-A2BD-48C1-966F-7B9E3DC37631}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
28 | {E8031950-A2BD-48C1-966F-7B9E3DC37631}.Debug|Any CPU.Build.0 = Debug|Any CPU
29 | {E8031950-A2BD-48C1-966F-7B9E3DC37631}.Release|Any CPU.ActiveCfg = Release|Any CPU
30 | {E8031950-A2BD-48C1-966F-7B9E3DC37631}.Release|Any CPU.Build.0 = Release|Any CPU
31 | EndGlobalSection
32 | GlobalSection(SolutionProperties) = preSolution
33 | HideSolutionNode = FALSE
34 | EndGlobalSection
35 | GlobalSection(ExtensibilityGlobals) = postSolution
36 | SolutionGuid = {FD437583-D2B4-4448-AF6E-F63E10906151}
37 | EndGlobalSection
38 | EndGlobal
39 |
--------------------------------------------------------------------------------
/Hangfire.Redis.Tests/Utils/RedisUtils.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using StackExchange.Redis;
3 |
4 | namespace Hangfire.Redis.Tests.Utils
5 | {
6 | public static class RedisUtils
7 | {
8 | private const string HostVariable = "Hangfire_Redis_Host";
9 | private const string PortVariable = "Hangfire_Redis_Port";
10 | private const string DbVariable = "Hangfire_Redis_Db";
11 |
12 | private const string DefaultHost = "127.0.0.1";
13 | private const int DefaultPort = 6379;
14 | private const int DefaultDb = 1;
15 | static Lazy connection = null;
16 |
17 | static RedisUtils()
18 | {
19 | connection = new Lazy(() =>
20 | {
21 | ConfigurationOptions options = new ConfigurationOptions
22 | {
23 | AllowAdmin = true,
24 | SyncTimeout = 15000,
25 | ConnectRetry = 5
26 | };
27 | options.EndPoints.Add(GetHostAndPort());
28 | return ConnectionMultiplexer.Connect(options);
29 | }
30 | );
31 | }
32 | public static IServer GetFirstServer()
33 | {
34 | return connection.Value.GetServer(connection.Value.GetEndPoints()[0]);
35 | }
36 | public static IDatabase CreateClient()
37 | {
38 | return connection.Value.GetDatabase(DefaultDb);
39 | }
40 |
41 | public static ISubscriber CreateSubscriber()
42 | {
43 | return connection.Value.GetSubscriber();
44 | }
45 |
46 | public static string GetHostAndPort()
47 | {
48 | return String.Format("{0}:{1}", GetHost(), GetPort());
49 | }
50 |
51 | public static string GetHost()
52 | {
53 | return Environment.GetEnvironmentVariable(HostVariable)
54 | ?? DefaultHost;
55 | }
56 |
57 | public static int GetPort()
58 | {
59 | var portValue = Environment.GetEnvironmentVariable(PortVariable);
60 | return portValue != null ? int.Parse(portValue) : DefaultPort;
61 | }
62 |
63 | public static int GetDb()
64 | {
65 | var dbValue = Environment.GetEnvironmentVariable(DbVariable);
66 | return dbValue != null ? int.Parse(dbValue) : DefaultDb;
67 | }
68 | }
69 | }
--------------------------------------------------------------------------------
/Hangfire.Redis.StackExchange/RedisDatabaseExtensions.cs:
--------------------------------------------------------------------------------
1 | // Copyright © 2013-2015 Sergey Odinokov, Marco Casamento
2 | // This software is based on https://github.com/HangfireIO/Hangfire.Redis
3 |
4 | // Hangfire.Redis.StackExchange is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as
6 | // published by the Free Software Foundation, either version 3
7 | // of the License, or any later version.
8 | //
9 | // Hangfire.Redis.StackExchange is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public
15 | // License along with Hangfire.Redis.StackExchange. If not, see .
16 |
17 | using System;
18 | using System.Collections.Generic;
19 | using System.Linq;
20 | using StackExchange.Redis;
21 |
22 | namespace Hangfire.Redis.StackExchange
23 | {
24 | public static class RedisDatabaseExtensions
25 | {
26 | public static HashEntry[] ToHashEntries(this IEnumerable> keyValuePairs)
27 | {
28 | var hashEntry = new HashEntry[keyValuePairs.Count()];
29 | int i = 0;
30 | foreach (var kvp in keyValuePairs)
31 | {
32 | hashEntry[i] = new HashEntry(kvp.Key, kvp.Value);
33 | i++;
34 | }
35 | return hashEntry;
36 | }
37 |
38 | public static RedisValue[] ToRedisValues(this IEnumerable values)
39 | {
40 | if (values == null)
41 | throw new ArgumentNullException(nameof(values));
42 |
43 | return values.Select(x => (RedisValue)x).ToArray();
44 | }
45 |
46 | //public static Dictionary ToStringDictionary(this HashEntry[] entries)
47 | //{
48 | // var dictionary = new Dictionary(entries.Length);
49 | // foreach (var entry in entries)
50 | // dictionary[entry.Name] = entry.Value;
51 | // return dictionary;
52 | //}
53 |
54 | public static Dictionary GetValuesMap(this IDatabase redis, string[] keys)
55 | {
56 | var redisKeyArr = keys.Select(x => (RedisKey)x).ToArray();
57 | var valuesArr = redis.StringGet(redisKeyArr);
58 | Dictionary result = new Dictionary(valuesArr.Length);
59 | for (int i = 0; i < valuesArr.Length; i++)
60 | {
61 | result.Add(redisKeyArr[i], valuesArr[i]);
62 | }
63 | return result;
64 | }
65 |
66 |
67 | }
68 |
69 | }
70 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/Hangfire.Redis.Tests/RedisStorageFacts.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using Hangfire.Logging;
4 | using Hangfire.Redis.StackExchange;
5 | using Hangfire.Redis.Tests.Utils;
6 | using Moq;
7 | using Xunit;
8 |
9 | namespace Hangfire.Redis.Tests
10 | {
11 | [Collection("Sequential")]
12 | public class RedisStorageFacts
13 | {
14 | [Fact, CleanRedis]
15 | public void GetStateHandlers_ReturnsAllHandlers()
16 | {
17 | var storage = CreateStorage();
18 |
19 | var handlers = storage.GetStateHandlers();
20 |
21 | var handlerTypes = handlers.Select(x => x.GetType()).ToArray();
22 | Assert.Contains(typeof(FailedStateHandler), handlerTypes);
23 | Assert.Contains(typeof(ProcessingStateHandler), handlerTypes);
24 | Assert.Contains(typeof(SucceededStateHandler), handlerTypes);
25 | Assert.Contains(typeof(DeletedStateHandler), handlerTypes);
26 | }
27 |
28 | [Fact]
29 | public void DbFromConnectionStringIsUsed()
30 | {
31 | var storage = new RedisStorage(String.Format("{0},defaultDatabase=5", RedisUtils.GetHostAndPort()));
32 | Assert.Equal(5, storage.Db);
33 | }
34 |
35 | [Fact]
36 | public void PasswordFromToStringIsNotShown()
37 | {
38 | string password = Guid.NewGuid().ToString("N");
39 | var storage = new RedisStorage(String.Format("{0},password={1}", RedisUtils.GetHostAndPort(), password));
40 | Assert.DoesNotContain(password, storage.ToString());
41 | }
42 |
43 | [Fact]
44 | public void PasswordFromWriteOptionsToLogIsNotShown()
45 | {
46 | string password = Guid.NewGuid().ToString("N");
47 | var storage = new RedisStorage(String.Format("{0},password={1}", RedisUtils.GetHostAndPort(), password));
48 |
49 | string loggedMessage = null;
50 |
51 | var logMock = new Mock();
52 |
53 | logMock.Setup(p => p.Log(LogLevel.Debug, null, null)).Returns(true); // logger.IsDebugEnabled()
54 | logMock.Setup(p => p.Log(
55 | LogLevel.Debug,
56 | It.Is>(f => f != null && f.Invoke().StartsWith("ConnectionString: ")),
57 | null))
58 | .Callback((LogLevel lvl, Func msg, Exception ex) => { loggedMessage = msg.Invoke(); })
59 | .Returns(true)
60 | .Verifiable();
61 |
62 | storage.WriteOptionsToLog(logMock.Object);
63 |
64 | logMock.Verify();
65 | Assert.DoesNotContain(password, loggedMessage);
66 | }
67 |
68 | private static RedisStorage CreateStorage()
69 | {
70 | var options = new RedisStorageOptions() { Db = RedisUtils.GetDb() };
71 | return new RedisStorage(RedisUtils.GetHostAndPort(), options);
72 | }
73 | }
74 | }
--------------------------------------------------------------------------------
/Hangfire.Redis.StackExchange/RedisStorageExtensions.cs:
--------------------------------------------------------------------------------
1 | // Copyright � 2013-2015 Sergey Odinokov, Marco Casamento
2 | // This software is based on https://github.com/HangfireIO/Hangfire.Redis
3 |
4 | // Hangfire.Redis.StackExchange is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as
6 | // published by the Free Software Foundation, either version 3
7 | // of the License, or any later version.
8 | //
9 | // Hangfire.Redis.StackExchange is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public
15 | // License along with Hangfire.Redis.StackExchange. If not, see .
16 |
17 | using System;
18 | using Hangfire.Annotations;
19 | using StackExchange.Redis;
20 |
21 | namespace Hangfire.Redis.StackExchange
22 | {
23 | public static class RedisStorageExtensions
24 | {
25 | public static IGlobalConfiguration UseRedisStorage(
26 | [NotNull] this IGlobalConfiguration configuration)
27 | {
28 | if (configuration == null) throw new ArgumentNullException(nameof(configuration));
29 | var storage = new RedisStorage();
30 | GlobalJobFilters.Filters.Add(new HangfireSubscriber());
31 | return configuration.UseStorage(storage);
32 | }
33 |
34 | public static IGlobalConfiguration UseRedisStorage(
35 | [NotNull] this IGlobalConfiguration configuration,
36 | [NotNull] IConnectionMultiplexer connectionMultiplexer,
37 | RedisStorageOptions options = null)
38 | {
39 | if (configuration == null) throw new ArgumentNullException(nameof(configuration));
40 | if (connectionMultiplexer == null) throw new ArgumentNullException(nameof(connectionMultiplexer));
41 | var storage = new RedisStorage(connectionMultiplexer, options);
42 | GlobalJobFilters.Filters.Add(new HangfireSubscriber());
43 | return configuration.UseStorage(storage);
44 | }
45 |
46 |
47 | public static IGlobalConfiguration UseRedisStorage(
48 | [NotNull] this IGlobalConfiguration configuration,
49 | [NotNull] string nameOrConnectionString,
50 | RedisStorageOptions options = null)
51 | {
52 | if (configuration == null) throw new ArgumentNullException(nameof(configuration));
53 | if (nameOrConnectionString == null) throw new ArgumentNullException(nameof(nameOrConnectionString));
54 | var storage = new RedisStorage(nameOrConnectionString, options);
55 | GlobalJobFilters.Filters.Add(new HangfireSubscriber());
56 | return configuration.UseStorage(storage);
57 | }
58 | }
59 | }
--------------------------------------------------------------------------------
/Hangfire.Redis.StackExchange/RedisStorageOptions.cs:
--------------------------------------------------------------------------------
1 | // Copyright © 2013-2015 Sergey Odinokov, Marco Casamento
2 | // This software is based on https://github.com/HangfireIO/Hangfire.Redis
3 |
4 | // Hangfire.Redis.StackExchange is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as
6 | // published by the Free Software Foundation, either version 3
7 | // of the License, or any later version.
8 | //
9 | // Hangfire.Redis.StackExchange is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public
15 | // License along with Hangfire.Redis.StackExchange. If not, see .
16 |
17 | using System;
18 |
19 | namespace Hangfire.Redis.StackExchange
20 | {
21 | public class RedisStorageOptions
22 | {
23 | public const string DefaultPrefix = "{hangfire}:";
24 |
25 | public RedisStorageOptions()
26 | {
27 | InvisibilityTimeout = TimeSpan.FromMinutes(30);
28 | FetchTimeout = TimeSpan.FromMinutes(3);
29 | ExpiryCheckInterval = TimeSpan.FromHours(1);
30 | Db = 0;
31 | Prefix = DefaultPrefix;
32 | SucceededListSize = 499;
33 | DeletedListSize = 499;
34 | LifoQueues = new string[0];
35 | UseTransactions = true;
36 | }
37 |
38 | ///
39 | /// It's a part of mechanism to requeue the job if the server processing it died for some reason
40 | ///
41 | ///
42 | public TimeSpan InvisibilityTimeout { get; set; }
43 |
44 | ///
45 | /// It's a fallback for fetching jobs if pub/sub mechanism fails. This software use redis pub/sub to be notified
46 | /// when a new job has been enqueued. Redis pub/sub however doesn't guarantee delivery so, should the
47 | /// RedisConnection lose a message, within the FetchTimeout the queue will be scanned from scratch
48 | /// and any waiting jobs will be processed.
49 | ///
50 | public TimeSpan FetchTimeout { get; set; }
51 |
52 | ///
53 | /// Time that should pass between expired jobs cleanup.
54 | /// RedisStorage uses non-expiring keys, so to clean up the store there is a thread running a IServerComponent
55 | /// that take care of deleting expired jobs from redis.
56 | ///
57 | public TimeSpan ExpiryCheckInterval { get; set; }
58 | public string Prefix { get; set; }
59 | public int Db { get; set; }
60 | public int SucceededListSize { get; set; }
61 | public int DeletedListSize { get; set; }
62 | public string[] LifoQueues { get; set; }
63 | public bool UseTransactions { get; set; }
64 | }
65 | }
--------------------------------------------------------------------------------
/Hangfire.Redis.Tests/ExpiredJobsWatcherFacts.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 | using Hangfire.Common;
4 | using Hangfire.Redis.StackExchange;
5 | using Hangfire.Redis.Tests.Utils;
6 | using Hangfire.Server;
7 | using Xunit;
8 |
9 | namespace Hangfire.Redis.Tests
10 | {
11 | [CleanRedis, Collection("Sequential")]
12 | public class ExpiredJobsWatcherFacts
13 | {
14 | private static readonly TimeSpan CheckInterval = TimeSpan.FromSeconds(1);
15 |
16 | private readonly RedisStorage _storage;
17 | private readonly CancellationTokenSource _cts;
18 |
19 | public ExpiredJobsWatcherFacts()
20 | {
21 | var options = new RedisStorageOptions() {Db = RedisUtils.GetDb()};
22 | _storage = new RedisStorage(RedisUtils.GetHostAndPort(), options);
23 | _cts = new CancellationTokenSource();
24 | _cts.Cancel();
25 | }
26 |
27 | [Fact]
28 | public void Ctor_ThrowsAnException_WhenStorageIsNull()
29 | {
30 | Assert.Throws("storage",
31 | () => new ExpiredJobsWatcher(null, CheckInterval));
32 | }
33 |
34 | [Fact]
35 | public void Ctor_ThrowsAnException_WhenCheckIntervalIsZero()
36 | {
37 | Assert.Throws("checkInterval",
38 | () => new ExpiredJobsWatcher(_storage, TimeSpan.Zero));
39 | }
40 |
41 | [Fact]
42 | public void Ctor_ThrowsAnException_WhenCheckIntervalIsNegative()
43 | {
44 | Assert.Throws("checkInterval",
45 | () => new ExpiredJobsWatcher(_storage, TimeSpan.FromSeconds(-1)));
46 | }
47 |
48 | [Fact, CleanRedis]
49 | public void Execute_DeletesNonExistingJobs()
50 | {
51 | var redis = RedisUtils.CreateClient();
52 |
53 | Assert.Equal(0, redis.ListLength("{hangfire}:succeeded"));
54 | Assert.Equal(0, redis.ListLength("{hangfire}:deleted"));
55 |
56 | // Arrange
57 | redis.ListRightPush("{hangfire}:succeded", "my-job");
58 | redis.ListRightPush("{hangfire}:deleted", "other-job");
59 |
60 | var watcher = CreateWatcher();
61 |
62 | // Act
63 | watcher.Execute(_cts.Token);
64 |
65 | // Assert
66 | Assert.Equal(0, redis.ListLength("{hangfire}:succeeded"));
67 | Assert.Equal(0, redis.ListLength("{hangfire}:deleted"));
68 | }
69 |
70 | [Fact, CleanRedis]
71 | public void Execute_DoesNotDeleteExistingJobs()
72 | {
73 | var redis = RedisUtils.CreateClient();
74 | // Arrange
75 | redis.ListRightPush("{hangfire}:succeeded", "my-job");
76 | redis.HashSet("{hangfire}:job:my-job", "Fetched",
77 | JobHelper.SerializeDateTime(DateTime.UtcNow.AddDays(-1)));
78 |
79 | redis.ListRightPush("{hangfire}:deleted", "other-job");
80 | redis.HashSet("{hangfire}:job:other-job", "Fetched",
81 | JobHelper.SerializeDateTime(DateTime.UtcNow.AddDays(-1)));
82 |
83 | var watcher = CreateWatcher();
84 |
85 | // Act
86 | watcher.Execute(_cts.Token);
87 |
88 | // Assert
89 | Assert.Equal(1, redis.ListLength("{hangfire}:succeeded"));
90 | Assert.Equal(1, redis.ListLength("{hangfire}:deleted"));
91 | }
92 |
93 | #pragma warning disable 618
94 | private IServerComponent CreateWatcher()
95 | #pragma warning restore 618
96 | {
97 | return new ExpiredJobsWatcher(_storage, CheckInterval);
98 | }
99 | }
100 | }
--------------------------------------------------------------------------------
/Hangfire.Redis.StackExchange/ExpiredJobsWatcher.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading;
5 | using System.Threading.Tasks;
6 | using Hangfire.Logging;
7 | using Hangfire.Server;
8 | using StackExchange.Redis;
9 |
10 | namespace Hangfire.Redis.StackExchange
11 | {
12 | #pragma warning disable 618
13 | internal class ExpiredJobsWatcher : IServerComponent
14 | #pragma warning restore 618
15 | {
16 | private static readonly ILog Logger = LogProvider.For();
17 |
18 | private readonly RedisStorage _storage;
19 | private readonly TimeSpan _checkInterval;
20 |
21 | private static readonly string[] ProcessedKeys =
22 | {
23 | "succeeded",
24 | "deleted"
25 | };
26 |
27 | public ExpiredJobsWatcher(RedisStorage storage, TimeSpan checkInterval)
28 | {
29 | if (storage == null)
30 | throw new ArgumentNullException(nameof(storage));
31 | if (checkInterval.Ticks <= 0)
32 | throw new ArgumentOutOfRangeException(nameof(checkInterval), "Check interval should be positive.");
33 |
34 | _storage = storage;
35 | _checkInterval = checkInterval;
36 | }
37 |
38 | public override string ToString()
39 | {
40 | return GetType().ToString();
41 | }
42 |
43 | void IServerComponent.Execute(CancellationToken cancellationToken)
44 | {
45 | using (var connection = (RedisConnection) _storage.GetConnection())
46 | {
47 | var redis = connection.Redis;
48 |
49 | foreach (var key in ProcessedKeys)
50 | {
51 | var redisKey = _storage.GetRedisKey(key);
52 |
53 | var count = redis.ListLength(redisKey);
54 | if (count == 0) continue;
55 |
56 | Logger.InfoFormat("Removing expired records from the '{0}' list...", key);
57 |
58 | const int batchSize = 100;
59 | var keysToRemove = new List();
60 |
61 | for (var last = count - 1; last >= 0; last -= batchSize)
62 | {
63 | var first = Math.Max(0, last - batchSize + 1);
64 |
65 | var jobIds = redis.ListRange(redisKey, first, last).ToStringArray();
66 | if (jobIds.Length == 0) continue;
67 |
68 | var pipeline = redis.CreateBatch();
69 | var tasks = new Task[jobIds.Length];
70 |
71 | for (var i = 0; i < jobIds.Length; i++)
72 | {
73 | tasks[i] = pipeline.KeyExistsAsync(_storage.GetRedisKey($"job:{jobIds[i]}"));
74 | }
75 |
76 | pipeline.Execute();
77 | Task.WaitAll(tasks);
78 |
79 | keysToRemove.AddRange(jobIds.Where((t, i) => !((Task) tasks[i]).Result));
80 | }
81 |
82 | if (keysToRemove.Count == 0) continue;
83 |
84 | Logger.InfoFormat("Removing {0} expired jobs from '{1}' list...", keysToRemove.Count, key);
85 |
86 | using (var transaction = connection.CreateWriteTransaction())
87 | {
88 | foreach (var jobId in keysToRemove)
89 | {
90 | transaction.RemoveFromList(key, jobId);
91 | }
92 |
93 | transaction.Commit();
94 | }
95 | }
96 | }
97 |
98 | cancellationToken.WaitHandle.WaitOne(_checkInterval);
99 | }
100 | }
101 | }
--------------------------------------------------------------------------------
/Hangfire.Redis.StackExchange/RedisFetchedJob.cs:
--------------------------------------------------------------------------------
1 | // Copyright © 2013-2015 Sergey Odinokov, Marco Casamento
2 | // This software is based on https://github.com/HangfireIO/Hangfire.Redis
3 |
4 | // Hangfire.Redis.StackExchange is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as
6 | // published by the Free Software Foundation, either version 3
7 | // of the License, or any later version.
8 | //
9 | // Hangfire.Redis.StackExchange is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public
15 | // License along with Hangfire.Redis.StackExchange. If not, see .
16 |
17 | using System;
18 | using Hangfire.Annotations;
19 | using Hangfire.Common;
20 | using Hangfire.Storage;
21 | using StackExchange.Redis;
22 |
23 | namespace Hangfire.Redis.StackExchange
24 | {
25 | internal class RedisFetchedJob : IFetchedJob
26 | {
27 | private readonly RedisStorage _storage;
28 | private readonly IDatabase _redis;
29 | private bool _disposed;
30 | private bool _removedFromQueue;
31 | private bool _requeued;
32 |
33 | public RedisFetchedJob(
34 | [NotNull] RedisStorage storage,
35 | [NotNull] IDatabase redis,
36 | [NotNull] string jobId,
37 | [NotNull] string queue,
38 | [CanBeNull] DateTime? fetchedAt)
39 | {
40 | _storage = storage ?? throw new ArgumentNullException(nameof(storage));
41 | _redis = redis ?? throw new ArgumentNullException(nameof(redis));
42 | JobId = jobId ?? throw new ArgumentNullException(nameof(jobId));
43 | Queue = queue ?? throw new ArgumentNullException(nameof(queue));
44 | FetchedAt = fetchedAt;
45 | }
46 |
47 | public string JobId { get; }
48 | public string Queue { get; }
49 | public DateTime? FetchedAt { get; }
50 |
51 | private DateTime? GetFetchedValue()
52 | {
53 | return JobHelper.DeserializeNullableDateTime(_redis.HashGet(_storage.GetRedisKey($"job:{JobId}"), "Fetched"));
54 | }
55 |
56 | public void RemoveFromQueue()
57 | {
58 | var fetchedAt = GetFetchedValue();
59 | if (_storage.UseTransactions)
60 | {
61 | var transaction = _redis.CreateTransaction();
62 |
63 | if (fetchedAt == FetchedAt)
64 | {
65 | RemoveFromFetchedListAsync(transaction);
66 | }
67 | transaction.PublishAsync(_storage.SubscriptionChannel, JobId);
68 | transaction.Execute();
69 | }
70 | else
71 | {
72 | if (fetchedAt == FetchedAt)
73 | {
74 | RemoveFromFetchedList(_redis);
75 | }
76 |
77 | _redis.Publish(_storage.SubscriptionChannel, JobId);
78 | }
79 | _removedFromQueue = true;
80 | }
81 |
82 | public void Requeue()
83 | {
84 | var fetchedAt = GetFetchedValue();
85 | if (_storage.UseTransactions)
86 | {
87 | var transaction = _redis.CreateTransaction();
88 | transaction.ListRightPushAsync(_storage.GetRedisKey($"queue:{Queue}"), JobId);
89 | if (fetchedAt == FetchedAt)
90 | {
91 | RemoveFromFetchedListAsync(transaction);
92 | }
93 |
94 | transaction.PublishAsync(_storage.SubscriptionChannel, JobId);
95 | transaction.Execute();
96 | } else
97 | {
98 | _redis.ListRightPush(_storage.GetRedisKey($"queue:{Queue}"), JobId);
99 | if (fetchedAt == FetchedAt)
100 | {
101 | RemoveFromFetchedList(_redis);
102 | }
103 |
104 | _redis.Publish(_storage.SubscriptionChannel, JobId);
105 | }
106 | _requeued = true;
107 | }
108 |
109 | public void Dispose()
110 | {
111 | if (_disposed) return;
112 |
113 | if (!_removedFromQueue && !_requeued)
114 | {
115 | Requeue();
116 | }
117 |
118 | _disposed = true;
119 | }
120 |
121 | private void RemoveFromFetchedListAsync(IDatabaseAsync databaseAsync)
122 | {
123 | databaseAsync.ListRemoveAsync(_storage.GetRedisKey($"queue:{Queue}:dequeued"), JobId, -1);
124 | databaseAsync.HashDeleteAsync(_storage.GetRedisKey($"job:{JobId}"), ["Fetched", "Checked"]);
125 | }
126 | private void RemoveFromFetchedList(IDatabase database)
127 | {
128 | database.ListRemove(_storage.GetRedisKey($"queue:{Queue}:dequeued"), JobId, -1);
129 | database.HashDelete(_storage.GetRedisKey($"job:{JobId}"), ["Fetched", "Checked"]);
130 | }
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/Hangfire.Redis.Tests/RedisLockFacts.cs:
--------------------------------------------------------------------------------
1 | using Hangfire.Storage;
2 | using StackExchange.Redis;
3 | using System;
4 | using System.Threading;
5 | using System.Threading.Tasks;
6 | using Hangfire.Redis.StackExchange;
7 | using Hangfire.Redis.Tests.Utils;
8 | using Xunit;
9 |
10 | namespace Hangfire.Redis.Tests
11 | {
12 | [Collection("Sequential")]
13 | public class RedisLockFacts
14 | {
15 | [Fact, CleanRedis]
16 | public void AcquireInSequence()
17 | {
18 | var db = RedisUtils.CreateClient();
19 |
20 | using (var testLock = RedisLock.Acquire(db, "testLock", TimeSpan.FromMilliseconds(1)))
21 | Assert.NotNull(testLock);
22 | using (var testLock = RedisLock.Acquire(db, "testLock", TimeSpan.FromMilliseconds(1)))
23 | Assert.NotNull(testLock);
24 | }
25 |
26 | [Fact, CleanRedis]
27 | public void AcquireNested()
28 | {
29 | var db = RedisUtils.CreateClient();
30 |
31 | using (var testLock1 = RedisLock.Acquire(db, "testLock", TimeSpan.FromMilliseconds(100)))
32 | {
33 | Assert.NotNull(testLock1);
34 |
35 | using (var testLock2 = RedisLock.Acquire(db, "testLock", TimeSpan.FromMilliseconds(100)))
36 | {
37 | Assert.NotNull(testLock2);
38 | }
39 | }
40 | }
41 |
42 | [Fact, CleanRedis]
43 | public void AcquireFromMultipleThreads()
44 | {
45 | var db = RedisUtils.CreateClient();
46 |
47 | var sync = new ManualResetEventSlim();
48 |
49 | var thread1 = new Thread(state =>
50 | {
51 | using (var testLock1 = RedisLock.Acquire(db, "test", TimeSpan.FromMilliseconds(50)))
52 | {
53 | // ensure nested lock release doesn't release parent lock
54 | using (var testLock2 = RedisLock.Acquire(db, "test", TimeSpan.FromMilliseconds(50)))
55 | {
56 | }
57 |
58 | sync.Set();
59 | Thread.Sleep(200);
60 | }
61 | });
62 |
63 | var thread2 = new Thread(state =>
64 | {
65 | Assert.True(sync.Wait(1000));
66 |
67 | Assert.Throws(() =>
68 | {
69 | using (var testLock2 = RedisLock.Acquire(db, "test", TimeSpan.FromMilliseconds(50)))
70 | {
71 | }
72 | });
73 | });
74 |
75 | thread1.Start();
76 | thread2.Start();
77 | thread1.Join();
78 | thread2.Join();
79 | }
80 |
81 | //private async Task NestedTask(IDatabase db)
82 | //{
83 |
84 | // await Task.Yield();
85 | // using var lestLock2 = RedisLock.Acquire(db, "test", TimeSpan.FromMilliseconds(10));
86 | // Assert.NotNull(lestLock2);
87 | //}
88 |
89 | //Test below will fail due to the changed storage model of HeldLocks in RedisLock (it should be AsyncLocal instead of ThreadLocal)
90 | //It seems that something is wrong when run in hangfire but I don't have a repro.
91 | //Giving that Hangfire as of 1.8.7 still doesn't use Tasks (milestone set for 2.0) I'm leaving HeldLocks in a ThreadLocal
92 | //and commenting out the failing test
93 |
94 | //[Fact, CleanRedis]
95 | //public async Task AcquireFromNestedTask()
96 | //{
97 | // var db = RedisUtils.CreateClient();
98 |
99 | // using var lock1 = RedisLock.Acquire(db, "test", TimeSpan.FromMilliseconds(50));
100 | // Assert.NotNull(lock1);
101 |
102 | // await Task.Delay(100);
103 |
104 | // await Task.Run(() => NestedTask(db));
105 |
106 | //}
107 |
108 | [Fact, CleanRedis]
109 | public void SlidingExpirationTest()
110 | {
111 | var db = RedisUtils.CreateClient();
112 |
113 | var sync1 = new ManualResetEventSlim();
114 | var sync2 = new ManualResetEventSlim();
115 |
116 | var thread1 = new Thread(state =>
117 | {
118 | using (var testLock1 = RedisLock.Acquire(db, "testLock", TimeSpan.FromMilliseconds(100), TimeSpan.FromMilliseconds(110)))
119 | {
120 | Assert.NotNull(testLock1);
121 |
122 | // sleep a bit more than holdDuration
123 | Thread.Sleep(250);
124 | sync1.Set();
125 | sync2.Wait();
126 | }
127 | });
128 |
129 | var thread2 = new Thread(state =>
130 | {
131 | Assert.True(sync1.Wait(1000));
132 |
133 | Assert.Throws(() =>
134 | {
135 | using (var testLock2 = RedisLock.Acquire(db, "testLock", TimeSpan.FromMilliseconds(100)))
136 | {
137 | }
138 | });
139 | });
140 |
141 | thread1.Start();
142 | thread2.Start();
143 | thread2.Join();
144 | sync2.Set();
145 | thread1.Join();
146 | }
147 | }
148 | }
--------------------------------------------------------------------------------
/Hangfire.Redis.StackExchange/RedisInfoKeys.cs:
--------------------------------------------------------------------------------
1 | namespace Hangfire.Redis.StackExchange
2 | {
3 | public class RedisInfoKeys
4 | {
5 | public static string redis_version = "redis_version";
6 | public static string redis_git_sha1 = "redis_git_sha1";
7 | public static string redis_git_dirty = "redis_git_dirty";
8 | public static string redis_build_id = "redis_build_id";
9 | public static string redis_mode = "redis_mode";
10 | public static string os = "os";
11 | public static string arch_bits = "arch_bits";
12 | public static string multiplexing_api = "multiplexing_api";
13 | public static string process_id = "process_id";
14 | public static string run_id = "run_id";
15 | public static string tcp_port = "tcp_port";
16 | public static string uptime_in_seconds = "uptime_in_seconds";
17 | public static string uptime_in_days = "uptime_in_days";
18 | public static string hz = "hz";
19 | public static string lru_clock = "lru_clock";
20 | public static string config_file = "config_file";
21 | public static string connected_clients = "connected_clients";
22 | public static string client_longest_output_list = "client_longest_output_list";
23 | public static string client_biggest_input_buf = "client_biggest_input_buf";
24 | public static string blocked_clients = "blocked_clients";
25 | public static string used_memory = "used_memory";
26 | public static string used_memory_human = "used_memory_human";
27 | public static string used_memory_rss = "used_memory_rss";
28 | public static string used_memory_peak = "used_memory_peak";
29 | public static string used_memory_peak_human = "used_memory_peak_human";
30 | public static string used_memory_lua = "used_memory_lua";
31 | public static string mem_fragmentation_ratio = "mem_fragmentation_ratio";
32 | public static string mem_allocator = "mem_allocator";
33 | public static string loading = "loading";
34 | public static string rdb_changes_since_last_save = "rdb_changes_since_last_save";
35 | public static string rdb_bgsave_in_progress = "rdb_bgsave_in_progress";
36 | public static string rdb_last_save_time = "rdb_last_save_time";
37 | public static string rdb_last_bgsave_status = "rdb_last_bgsave_status";
38 | public static string rdb_last_bgsave_time_sec = "rdb_last_bgsave_time_sec";
39 | public static string rdb_current_bgsave_time_sec = "rdb_current_bgsave_time_sec";
40 | public static string aof_enabled = "aof_enabled";
41 | public static string aof_rewrite_in_progress = "aof_rewrite_in_progress";
42 | public static string aof_rewrite_scheduled = "aof_rewrite_scheduled";
43 | public static string aof_last_rewrite_time_sec = "aof_last_rewrite_time_sec";
44 | public static string aof_current_rewrite_time_sec = "aof_current_rewrite_time_sec";
45 | public static string aof_last_bgrewrite_status = "aof_last_bgrewrite_status";
46 | public static string aof_last_write_status = "aof_last_write_status";
47 | public static string total_connections_received = "total_connections_received";
48 | public static string total_commands_processed = "total_commands_processed";
49 | public static string instantaneous_ops_per_sec = "instantaneous_ops_per_sec";
50 | public static string total_net_input_bytes = "total_net_input_bytes";
51 | public static string total_net_output_bytes = "total_net_output_bytes";
52 | public static string instantaneous_input_kbps = "instantaneous_input_kbps";
53 | public static string instantaneous_output_kbps = "instantaneous_output_kbps";
54 | public static string rejected_connections = "rejected_connections";
55 | public static string sync_full = "sync_full";
56 | public static string sync_partial_ok = "sync_partial_ok";
57 | public static string sync_partial_err = "sync_partial_err";
58 | public static string expired_keys = "expired_keys";
59 | public static string evicted_keys = "evicted_keys";
60 | public static string keyspace_hits = "keyspace_hits";
61 | public static string keyspace_misses = "keyspace_misses";
62 | public static string pubsub_channels = "pubsub_channels";
63 | public static string pubsub_patterns = "pubsub_patterns";
64 | public static string latest_fork_usec = "latest_fork_usec";
65 | public static string migrate_cached_sockets = "migrate_cached_sockets";
66 | public static string role = "role";
67 | public static string connected_slaves = "connected_slaves";
68 | public static string master_repl_offset = "master_repl_offset";
69 | public static string repl_backlog_active = "repl_backlog_active";
70 | public static string repl_backlog_size = "repl_backlog_size";
71 | public static string repl_backlog_first_byte_offset = "repl_backlog_first_byte_offset";
72 | public static string repl_backlog_histlen = "repl_backlog_histlen";
73 | public static string used_cpu_sys = "used_cpu_sys";
74 | public static string used_cpu_user = "used_cpu_user";
75 | public static string used_cpu_sys_children = "used_cpu_sys_children";
76 | public static string used_cpu_user_children = "used_cpu_user_children";
77 | public static string cluster_enabled = "cluster_enabled";
78 | public static string db0 = "db0";
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/Hangfire.Redis.Tests/FetchedJobsWatcherFacts.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 | using Hangfire.Common;
4 | using Hangfire.Redis.StackExchange;
5 | using Hangfire.Redis.Tests.Utils;
6 | using Xunit;
7 |
8 | namespace Hangfire.Redis.Tests
9 | {
10 | [CleanRedis, Collection("Sequential")]
11 | public class FetchedJobsWatcherFacts
12 | {
13 | private static readonly TimeSpan InvisibilityTimeout = TimeSpan.FromSeconds(10);
14 |
15 | private readonly RedisStorage _storage;
16 | private readonly CancellationTokenSource _cts;
17 |
18 | public FetchedJobsWatcherFacts()
19 | {
20 | var options = new RedisStorageOptions() {Db = RedisUtils.GetDb()};
21 | _storage = new RedisStorage(RedisUtils.GetHostAndPort(), options);
22 | _cts = new CancellationTokenSource();
23 | _cts.Cancel();
24 | }
25 |
26 | [Fact]
27 | public void Ctor_ThrowsAnException_WhenStorageIsNull()
28 | {
29 | Assert.Throws("storage",
30 | () => new FetchedJobsWatcher(null, InvisibilityTimeout));
31 | }
32 |
33 | [Fact]
34 | public void Ctor_ThrowsAnException_WhenInvisibilityTimeoutIsZero()
35 | {
36 | Assert.Throws("invisibilityTimeout",
37 | () => new FetchedJobsWatcher(_storage, TimeSpan.Zero));
38 | }
39 |
40 | [Fact]
41 | public void Ctor_ThrowsAnException_WhenInvisibilityTimeoutIsNegative()
42 | {
43 | Assert.Throws("invisibilityTimeout",
44 | () => new FetchedJobsWatcher(_storage, TimeSpan.FromSeconds(-1)));
45 | }
46 |
47 | [Fact]
48 | public void Execute_EnqueuesTimedOutJobs_AndDeletesThemFromFetchedList()
49 | {
50 | var redis = RedisUtils.CreateClient();
51 | // Arrange
52 | redis.SetAdd("{hangfire}:queues", "my-queue");
53 | redis.ListRightPush("{hangfire}:queue:my-queue:dequeued", "my-job");
54 | redis.HashSet("{hangfire}:job:my-job", "Fetched",
55 | JobHelper.SerializeDateTime(DateTime.UtcNow.AddDays(-1)));
56 |
57 | var watcher = CreateWatcher();
58 |
59 | // Act
60 | watcher.Execute(_cts.Token);
61 |
62 | // Assert
63 | Assert.Equal(0, redis.ListLength("{hangfire}:queue:my-queue:dequeued"));
64 |
65 | var listEntry = (string) redis.ListRightPop("{hangfire}:queue:my-queue");
66 | Assert.Equal("my-job", listEntry);
67 |
68 | var job = redis.HashGetAll("{hangfire}:job:my-job");
69 | Assert.DoesNotContain(job, x => x.Name == "Fetched");
70 | }
71 |
72 | [Fact, CleanRedis]
73 | public void Execute_MarksDequeuedJobAsChecked_IfItHasNoFetchedFlagSet()
74 | {
75 | var redis = RedisUtils.CreateClient();
76 | // Arrange
77 | redis.SetAdd("{hangfire}:queues", "my-queue");
78 | redis.ListRightPush("{hangfire}:queue:my-queue:dequeued", "my-job");
79 |
80 | var watcher = CreateWatcher();
81 |
82 | // Act
83 | watcher.Execute(_cts.Token);
84 |
85 | Assert.NotNull(JobHelper.DeserializeNullableDateTime(
86 | redis.HashGet("{hangfire}:job:my-job", "Checked")));
87 | }
88 |
89 | [Fact, CleanRedis]
90 | public void Execute_EnqueuesCheckedAndTimedOutJob_IfNoFetchedFlagSet()
91 | {
92 | var redis = RedisUtils.CreateClient();
93 | // Arrange
94 | redis.SetAdd("{hangfire}:queues", "my-queue");
95 | redis.ListRightPush("{hangfire}:queue:my-queue:dequeued", "my-job");
96 | redis.HashSet("{hangfire}:job:my-job", "Checked",
97 | JobHelper.SerializeDateTime(DateTime.UtcNow.AddDays(-1)));
98 |
99 | var watcher = CreateWatcher();
100 |
101 | // Act
102 | watcher.Execute(_cts.Token);
103 |
104 | // Arrange
105 | Assert.Equal(0, redis.ListLength("{hangfire}:queue:my-queue:dequeued"));
106 | Assert.Equal(1, redis.ListLength("{hangfire}:queue:my-queue"));
107 |
108 | var job = redis.HashGetAll("{hangfire}:job:my-job");
109 | Assert.DoesNotContain(job, x => x.Name == "Checked");
110 | }
111 |
112 | [Fact, CleanRedis]
113 | public void Execute_DoesNotEnqueueTimedOutByCheckedFlagJob_IfFetchedFlagSet()
114 | {
115 | var redis = RedisUtils.CreateClient();
116 |
117 | // Arrange
118 | redis.SetAdd("{hangfire}:queues", "my-queue");
119 | redis.ListRightPush("{hangfire}:queue:my-queue:dequeued", "my-job");
120 | redis.HashSet("{hangfire}:job:my-job", "Checked",
121 | JobHelper.SerializeDateTime(DateTime.UtcNow.AddDays(-1)));
122 | redis.HashSet("{hangfire}:job:my-job", "Fetched",
123 | JobHelper.SerializeDateTime(DateTime.UtcNow));
124 |
125 | var watcher = CreateWatcher();
126 |
127 | // Act
128 | watcher.Execute(_cts.Token);
129 |
130 | // Assert
131 | Assert.Equal(1, redis.ListLength("{hangfire}:queue:my-queue:dequeued"));
132 | }
133 |
134 | private FetchedJobsWatcher CreateWatcher()
135 | {
136 | return new FetchedJobsWatcher(_storage, InvisibilityTimeout);
137 | }
138 | }
139 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 |
4 | # User-specific files
5 | *.suo
6 | *.user
7 | *.userosscache
8 | *.sln.docstates
9 |
10 | # User-specific files (MonoDevelop/Xamarin Studio)
11 | *.userprefs
12 |
13 | # Build results
14 | [Dd]ebug/
15 | [Dd]ebugPublic/
16 | [Rr]elease/
17 | [Rr]eleases/
18 | x64/
19 | x86/
20 | bld/
21 | [Bb]in/
22 | [Oo]bj/
23 | [Ll]og/
24 |
25 | # Visual Studio 2015 cache/options directory
26 | .vs/
27 | # Uncomment if you have tasks that create the project's static files in wwwroot
28 | #wwwroot/
29 |
30 | # MSTest test Results
31 | [Tt]est[Rr]esult*/
32 | [Bb]uild[Ll]og.*
33 |
34 | # NUNIT
35 | *.VisualState.xml
36 | TestResult.xml
37 |
38 | # Build Results of an ATL Project
39 | [Dd]ebugPS/
40 | [Rr]eleasePS/
41 | dlldata.c
42 |
43 | # DNX
44 | project.lock.json
45 | project.fragment.lock.json
46 | artifacts/
47 |
48 | *_i.c
49 | *_p.c
50 | *_i.h
51 | *.ilk
52 | *.meta
53 | *.obj
54 | *.pch
55 | *.pdb
56 | *.pgc
57 | *.pgd
58 | *.rsp
59 | *.sbr
60 | *.tlb
61 | *.tli
62 | *.tlh
63 | *.tmp
64 | *.tmp_proj
65 | *.log
66 | *.vspscc
67 | *.vssscc
68 | .builds
69 | *.pidb
70 | *.svclog
71 | *.scc
72 |
73 | # Chutzpah Test files
74 | _Chutzpah*
75 |
76 | # Visual C++ cache files
77 | ipch/
78 | *.aps
79 | *.ncb
80 | *.opendb
81 | *.opensdf
82 | *.sdf
83 | *.cachefile
84 | *.VC.db
85 | *.VC.VC.opendb
86 |
87 | # Visual Studio profiler
88 | *.psess
89 | *.vsp
90 | *.vspx
91 | *.sap
92 |
93 | # TFS 2012 Local Workspace
94 | $tf/
95 |
96 | # Guidance Automation Toolkit
97 | *.gpState
98 |
99 | # ReSharper is a .NET coding add-in
100 | _ReSharper*/
101 | *.[Rr]e[Ss]harper
102 | *.DotSettings.user
103 |
104 | # JustCode is a .NET coding add-in
105 | .JustCode
106 |
107 | # TeamCity is a build add-in
108 | _TeamCity*
109 |
110 | # DotCover is a Code Coverage Tool
111 | *.dotCover
112 |
113 | # NCrunch
114 | _NCrunch_*
115 | .*crunch*.local.xml
116 | nCrunchTemp_*
117 |
118 | # MightyMoose
119 | *.mm.*
120 | AutoTest.Net/
121 |
122 | # Web workbench (sass)
123 | .sass-cache/
124 |
125 | # Installshield output folder
126 | [Ee]xpress/
127 |
128 | # DocProject is a documentation generator add-in
129 | DocProject/buildhelp/
130 | DocProject/Help/*.HxT
131 | DocProject/Help/*.HxC
132 | DocProject/Help/*.hhc
133 | DocProject/Help/*.hhk
134 | DocProject/Help/*.hhp
135 | DocProject/Help/Html2
136 | DocProject/Help/html
137 |
138 | # Click-Once directory
139 | publish/
140 |
141 | # Publish Web Output
142 | *.[Pp]ublish.xml
143 | *.azurePubxml
144 | # TODO: Comment the next line if you want to checkin your web deploy settings
145 | # but database connection strings (with potential passwords) will be unencrypted
146 | #*.pubxml
147 | *.publishproj
148 |
149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
150 | # checkin your Azure Web App publish settings, but sensitive information contained
151 | # in these scripts will be unencrypted
152 | PublishScripts/
153 |
154 | # NuGet Packages
155 | *.nupkg
156 | # The packages folder can be ignored because of Package Restore
157 | **/packages/*
158 | # except build/, which is used as an MSBuild target.
159 | !**/packages/build/
160 | # Uncomment if necessary however generally it will be regenerated when needed
161 | #!**/packages/repositories.config
162 | # NuGet v3's project.json files produces more ignoreable files
163 | *.nuget.props
164 | *.nuget.targets
165 |
166 | # Microsoft Azure Build Output
167 | csx/
168 | *.build.csdef
169 |
170 | # Microsoft Azure Emulator
171 | ecf/
172 | rcf/
173 |
174 | # Windows Store app package directories and files
175 | AppPackages/
176 | BundleArtifacts/
177 | Package.StoreAssociation.xml
178 | _pkginfo.txt
179 |
180 | # Visual Studio cache files
181 | # files ending in .cache can be ignored
182 | *.[Cc]ache
183 | # but keep track of directories ending in .cache
184 | !*.[Cc]ache/
185 |
186 | # Others
187 | ClientBin/
188 | ~$*
189 | *~
190 | *.dbmdl
191 | *.dbproj.schemaview
192 | *.jfm
193 | *.pfx
194 | *.publishsettings
195 | node_modules/
196 | orleans.codegen.cs
197 |
198 | # Since there are multiple workflows, uncomment next line to ignore bower_components
199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
200 | #bower_components/
201 |
202 | # RIA/Silverlight projects
203 | Generated_Code/
204 |
205 | # Backup & report files from converting an old project file
206 | # to a newer Visual Studio version. Backup files are not needed,
207 | # because we have git ;-)
208 | _UpgradeReport_Files/
209 | Backup*/
210 | UpgradeLog*.XML
211 | UpgradeLog*.htm
212 |
213 | # SQL Server files
214 | *.mdf
215 | *.ldf
216 |
217 | # Business Intelligence projects
218 | *.rdl.data
219 | *.bim.layout
220 | *.bim_*.settings
221 |
222 | # Microsoft Fakes
223 | FakesAssemblies/
224 |
225 | # GhostDoc plugin setting file
226 | *.GhostDoc.xml
227 |
228 | # Node.js Tools for Visual Studio
229 | .ntvs_analysis.dat
230 |
231 | # Visual Studio 6 build log
232 | *.plg
233 |
234 | # Visual Studio 6 workspace options file
235 | *.opt
236 |
237 | # Visual Studio LightSwitch build output
238 | **/*.HTMLClient/GeneratedArtifacts
239 | **/*.DesktopClient/GeneratedArtifacts
240 | **/*.DesktopClient/ModelManifest.xml
241 | **/*.Server/GeneratedArtifacts
242 | **/*.Server/ModelManifest.xml
243 | _Pvt_Extensions
244 |
245 | # Paket dependency manager
246 | .paket/paket.exe
247 | paket-files/
248 |
249 | # FAKE - F# Make
250 | .fake/
251 |
252 | # JetBrains Rider
253 | .idea/
254 | *.sln.iml
255 |
256 | # CodeRush
257 | .cr/
258 |
259 | # Python Tools for Visual Studio (PTVS)
260 | __pycache__/
261 | *.pyc
262 | /dump.rdb
263 | /dump.rdb
264 |
--------------------------------------------------------------------------------
/Hangfire.Redis.StackExchange/Hangfire.Redis.StackExchange.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | netstandard2.0
4 | 12
5 | Hangfire.Redis.StackExchange
6 | true
7 | 1.12.0
8 | 1.12.0
9 | Marco Casamento and contributors
10 | Hangfire Redis Storage
11 | https://github.com/marcoCasamento/Hangfire.Redis.StackExchange
12 |
13 | Hangfire Redis Storage Based on Redis.StackExchange
14 | See http://hangfire.io/ for some more info on the main project and https://github.com/marcoCasamento/Hangfire.Redis.StackExchange for info on this
15 |
16 | README.md
17 |
18 | Enjoy Redis outstanding performance when fetching your jobs!
19 | Hangfire Redis storage use Redis to persists Job information, through Redis.StackExchange library.
20 | It also supports Batches (Pro Feature)
21 |
22 |
23 | 1.12.0 Fix #153 and all the chaneg
24 | - Resume all changes between 1after 1.9.3 4 , realigning the codegit
25 | - Bump Hangfire to 1.8.12 and StackExchange.Redis to 2.7.33
26 | 1.11.0 Fix #157 AND bring back the code from 1.9.4
27 | With this release I'm trying to start back from 1.9.4 (Latest release "approved for production" as 1.9.5 got unlisted) and I'm willing to review and recommit the code from there. One commit after the other.
28 | If anyone can help with testing, please drop a line to marco.casamento@gmail.com
29 | 1.10.1-Beta2
30 | - Fix #153 Appeared after 1.10.0 release. Only updates to this version for testing purposes and PLEASE report any issues. Stick to 1.9.3 for production.
31 | 1.10.0
32 | - Fix #135 -CHANGED BEHAVIOR- Library now requeue jobs from dead server (read comments in the issue) thanks to @Lexy2
33 | - Implement #150 (Replace async Redis calls with sync ones) thanks to @Lexy2
34 | - Fix #148 (Can't schedule a job without specifying the queue ) thanks to @Lexy2
35 | - Fix #142 (Scheduled jobs are returned incorrectly by the monitoring API) thanks to @Lexy2
36 | 1.9.5 ---UNLISTED----
37 | - Address #139 support for netstandard2.0
38 | - Fix #145 Added Job Queue parameter to the job information
39 | - Update to Hangfire 1.8.12 and StackExchange.Redis 2.7.33
40 | Big thanks to @Lexy2 for all the work on this release
41 | 1.9.4
42 | - Update to Hangfire 1.8.7
43 | - Add Support for all features defined for the storage in Hangfire 1.8.7
44 | - Update StackExchange.Redis to 2.7.10
45 | 1.9.3
46 | - Fix the missing key prefixing ({hangfire}:) for GetSetCount and GetSetContains (thanks to BobSilent)
47 | 1.9.2
48 | - Failed jobs page lists new items first (thanks to toddlucas)
49 | 1.9.1
50 | - Downgrade to netStandard 2.0 for broader compatibility
51 | 1.9
52 | - Switched AsyncLocal storage of redis lock to ThreadLocal
53 | - BREAKING CHANGE: Drop support for net461
54 | - Added compatibility to Hangfire.Core 1.8
55 | - BREAKING CHANGE: Namespace changes to uniform with folders (It wrongly appeared in previous ver)
56 | 1.8.5
57 | - Speed up Recurring jobs Fetching (thanks to developingjim)
58 | 1.8.4
59 | - Fix #90, #91 concurrent access on dictinoary (thanks to luukholleman)
60 | - Fix #94 occasional error in monitoringApi (thanks to tsu1980)
61 | 1.8.3
62 | - Removed dependency to NewtonSoft.JSON (thanks to neyromant)
63 | 1.8.2
64 | - Updated Hangfire to 1.7.11 (thanks to abarducci)
65 | - Updated StackExchange.Redis to 2.1.30 (thanks to abarducci)
66 | 1.8.1
67 | - Updated Hangfire to 1.7.8
68 | - Fixed #82 (thanks to @bcuff)
69 | 1.8.0
70 | - Updated StackExchange.Redis to 2.0 (thanks to @andrebires)
71 | 1.7.2
72 | - Added support for Lifo Queues (thanks to AndreSteenbergen)
73 | - Added option to not use transaction (thanks to AndreSteenbergen)
74 | - Enabled sliding expiration for distributed locks (thanks to pieceofsummer)
75 | - Add epired jobs watch to cleanup succeeded/deleted lists (thanks to pieceofsummer)
76 | - Make succeeded and deleted lists size configurable (thanks to pieceofsummer)
77 | - Fix job state order (thanks to pieceofsummer)
78 | - Exclude Fetched property from job parameters (thanks to pieceofsummer)
79 | 1.7.1
80 | - Add expired jobs watcher to cleanup succeeded/deleted lists thanks to pieceofsummer
81 | 1.7.0
82 | - Redis Cluster support (#42 thanks to gzpbx)
83 | - Update to VS2017 (#48 thanks to ryanelian)
84 |
85 | Hangfire Redis
86 | https://github.com/marcoCasamento/Hangfire.Redis.StackExchange
87 | git
88 | True
89 | false
90 | Hangfire_Redis_StackExchange.snk
91 | LGPL-3.0-or-later
92 | false
93 | disable
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 | all
104 | runtime; build; native; contentfiles; analyzers; buildtransitive
105 |
106 |
107 |
108 |
109 |
110 |
111 |
--------------------------------------------------------------------------------
/Hangfire.Redis.StackExchange/RedisLock.cs:
--------------------------------------------------------------------------------
1 | // Copyright © 2013-2015 Sergey Odinokov, Marco Casamento
2 | // This software is based on https://github.com/HangfireIO/Hangfire.Redis
3 |
4 | // Hangfire.Redis.StackExchange is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as
6 | // published by the Free Software Foundation, either version 3
7 | // of the License, or any later version.
8 | //
9 | // Hangfire.Redis.StackExchange is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public
15 | // License along with Hangfire.Redis.StackExchange. If not, see .
16 |
17 | using System;
18 | using System.Collections.Concurrent;
19 | using System.Diagnostics;
20 | using System.Threading;
21 | using Hangfire.Annotations;
22 | using Hangfire.Server;
23 | using Hangfire.States;
24 | using Hangfire.Storage;
25 | using StackExchange.Redis;
26 |
27 | namespace Hangfire.Redis.StackExchange
28 | {
29 | internal class RedisLock : IDisposable
30 | {
31 | private static readonly TimeSpan DefaultHoldDuration = TimeSpan.FromSeconds(30);
32 | private static readonly string OwnerId = Guid.NewGuid().ToString();
33 |
34 |
35 | private static ThreadLocal> _heldLocks = new ThreadLocal>();
36 |
37 | class StateBag
38 | {
39 | public PerformingContext PerformingContext { get; set; }
40 | public TimeSpan TimeSpan { get; set; }
41 | }
42 |
43 | private static ConcurrentDictionary HeldLocks
44 | {
45 | get
46 | {
47 | var value = _heldLocks.Value;
48 | if (value == null)
49 | _heldLocks.Value = value = new ConcurrentDictionary();
50 | return value;
51 | }
52 | }
53 |
54 | private readonly IDatabase _redis;
55 | private readonly RedisKey _key;
56 | private readonly bool _holdsLock;
57 | private volatile bool _isDisposed = false;
58 | private readonly Timer _slidingExpirationTimer;
59 |
60 | private RedisLock([NotNull] IDatabase redis, RedisKey key, bool holdsLock, TimeSpan holdDuration)
61 | {
62 | _redis = redis;
63 | _key = key;
64 | _holdsLock = holdsLock;
65 |
66 | if (holdsLock)
67 | {
68 | HeldLocks.TryAdd(_key, 1);
69 |
70 | // start sliding expiration timer at half timeout intervals
71 | var halfLockHoldDuration = TimeSpan.FromTicks(holdDuration.Ticks / 2);
72 | var state = new StateBag() { PerformingContext = HangfireSubscriber.Value, TimeSpan = holdDuration };
73 | _slidingExpirationTimer = new Timer(ExpirationTimerTick, state, halfLockHoldDuration, halfLockHoldDuration);
74 | }
75 | }
76 |
77 | private void ExpirationTimerTick(object state)
78 | {
79 | if (!_isDisposed)
80 | {
81 | var stateBag = state as StateBag;
82 | Exception redisEx = null;
83 | bool lockSuccesfullyExtended = false;
84 | int retryCount = 10;
85 | while (!lockSuccesfullyExtended && retryCount >= 0)
86 | {
87 | try
88 | {
89 | lockSuccesfullyExtended = _redis.LockExtend(_key, OwnerId, stateBag.TimeSpan);
90 | }
91 | catch (Exception ex)
92 | {
93 | redisEx = ex;
94 | Thread.Sleep(3000);
95 | retryCount--;
96 | }
97 | }
98 |
99 | if (!lockSuccesfullyExtended)
100 | {
101 | new BackgroundJobClient(stateBag.PerformingContext.Storage)
102 | .ChangeState(
103 | stateBag.PerformingContext.BackgroundJob.Id,
104 | new FailedState(new Exception($"Unable to extend a distributed lock with Key {_key} and OwnerId {OwnerId}", redisEx))
105 | );
106 | }
107 | }
108 | }
109 |
110 | public void Dispose()
111 | {
112 | if (_holdsLock)
113 | {
114 | _isDisposed = true;
115 | _slidingExpirationTimer.Dispose();
116 |
117 | if (!_redis.LockRelease(_key, OwnerId))
118 | {
119 | Debug.WriteLine("Lock {0} already timed out", _key);
120 | }
121 |
122 | HeldLocks.TryRemove(_key, out _);
123 | }
124 | }
125 |
126 | public static IDisposable Acquire([NotNull] IDatabase redis, RedisKey key, TimeSpan timeOut)
127 | {
128 | return Acquire(redis, key, timeOut, DefaultHoldDuration);
129 | }
130 |
131 | internal static IDisposable Acquire([NotNull] IDatabase redis, RedisKey key, TimeSpan timeOut, TimeSpan holdDuration)
132 | {
133 | if (redis == null)
134 | throw new ArgumentNullException(nameof(redis));
135 |
136 | if (HeldLocks.ContainsKey(key))
137 | {
138 | // lock is already held
139 | return new RedisLock(redis, key, false, holdDuration);
140 | }
141 |
142 | // The comparison below uses timeOut as a max timeSpan in waiting Lock
143 | var i = 0;
144 | var lockExpirationTime = DateTime.UtcNow + timeOut;
145 | do
146 | {
147 | if (redis.LockTake(key, OwnerId, holdDuration))
148 | {
149 | // we have successfully acquired the lock
150 | return new RedisLock(redis, key, true, holdDuration);
151 | }
152 |
153 | SleepBackOffMultiplier(i++, (int)(lockExpirationTime - DateTime.UtcNow).TotalMilliseconds);
154 | }
155 | while (DateTime.UtcNow < lockExpirationTime);
156 |
157 | throw new DistributedLockTimeoutException($"Failed to acquire lock on {key} within given timeout ({timeOut})");
158 | }
159 |
160 | private static void SleepBackOffMultiplier(int i, int maxWait)
161 | {
162 | if (maxWait <= 0) return;
163 |
164 | // exponential/random retry back-off.
165 | var rand = new Random(Guid.NewGuid().GetHashCode());
166 | var nextTry = rand.Next(
167 | (int)Math.Pow(i, 2), (int)Math.Pow(i + 1, 2) + 1);
168 |
169 | nextTry = Math.Min(nextTry, maxWait);
170 |
171 | Thread.Sleep(nextTry);
172 | }
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Hangfire.Redis.StackExchange
2 |
3 | HangFire Redis storage based on [HangFire.Redis](https://github.com/HangfireIO/Hangfire.Redis/) but using lovely [StackExchange.Redis](https://github.com/StackExchange/StackExchange.Redis) client.
4 |
5 | [](https://ci.appveyor.com/project/marcoCasamento/hangfire-redis-stackexchange)
6 | [](https://www.nuget.org/packages/Hangfire.Redis.StackExchange/)
7 |
8 | | Package Name | NuGet.org |
9 | |-----------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
10 | | `Hangfire.Redis.StackExchange` | [](https://www.nuget.org/packages/Hangfire.Redis.StackExchange/) |
11 | | `Hangfire.Redis.StackExchange.StrongName` | [](https://www.nuget.org/packages/Hangfire.Redis.StackExchange.StrongName/) |
12 |
13 | #### Highlights
14 | - Support for Hangfire Batches ([feature of Hangfire Pro](http://hangfire.io/blog/2015/04/17/hangfire-pro-1.2.0-released.html))
15 | - Efficient use of Redis resources thanks to ConnectionMultiplexer
16 | - Support for Redis Prefix, allow multiple Hangfire Instances against a single Redis DB
17 | - Allow customization of Succeeded and Failed lists size
18 |
19 | > Despite the name, `Hangfire.Redis.StackExchange.StrongName` is **not signed** because `Hangfire.Core` is not yet signed.
20 |
21 | ## Tutorial: Hangfire on Redis on ASP.NET Core MVC
22 |
23 | ### Getting Started
24 |
25 | To use Hangfire against Redis in an ASP.NET Core MVC projects, first you will need to install at least these two packages: `Hangfire.AspNetCore` and `Hangfire.Redis.StackExchange`.
26 |
27 | In `Startup.cs`, these are the **bare minimum** codes that you will need to write for enabling Hangfire on Redis:
28 |
29 | ```cs
30 | public class Startup
31 | {
32 | public static ConnectionMultiplexer Redis;
33 |
34 | public Startup(IHostingEnvironment env)
35 | {
36 | // Other codes / configurations are omitted for brevity.
37 | Redis = ConnectionMultiplexer.Connect(Configuration.GetConnectionString("Redis"));
38 | }
39 |
40 | public void ConfigureServices(IServiceCollection services)
41 | {
42 | services.AddHangfire(configuration =>
43 | {
44 | configuration.UseRedisStorage(Redis);
45 | });
46 | }
47 |
48 | public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
49 | {
50 | app.UseHangfireServer();
51 | }
52 | }
53 | ```
54 |
55 | > **Attention:** If you are using `Microsoft.Extensions.Caching.Redis` package, you will need to use `Hangfire.Redis.StackExchange.StrongName` instead, because the former package requires `StackExchange.Redis.StrongName` instead of `StackExchange.Redis`!
56 |
57 | ### Configurations
58 |
59 | #### configuration.UseRedisStorage(...)
60 |
61 | This method accepts two parameters:
62 |
63 | - The first parameter accepts either your Redis connection string or a `ConnectionMultiplexer` object. By [recommendation of the official StackExchange.Redis documentation](https://stackexchange.github.io/StackExchange.Redis/Basics), it is actually recommended to create one `ConnectionMultiplexer` for multiple reuse.
64 |
65 | - The second parameter accepts `RedisStorageOptions` object. As of version 1.7.0, these are the properties you can set into the said object:
66 |
67 | ```cs
68 | namespace Hangfire.Redis
69 | {
70 | public class RedisStorageOptions
71 | {
72 | public const string DefaultPrefix = "{hangfire}:";
73 |
74 | public RedisStorageOptions();
75 |
76 | public TimeSpan InvisibilityTimeout { get; set; }
77 | public TimeSpan FetchTimeout { get; set; }
78 | public string Prefix { get; set; }
79 | public int Db { get; set; }
80 | public int SucceededListSize { get; set; }
81 | public int DeletedListSize { get; set; }
82 | }
83 | }
84 | ```
85 |
86 | > It is **highly recommended** to set the **Prefix** property, to avoid overlap with other projects that targets the same Redis store!
87 |
88 | #### app.UseHangfireServer(...)
89 |
90 | This method accepts `BackgroundJobServerOptions` as the first parameter:
91 |
92 | ```cs
93 | namespace Hangfire
94 | {
95 | public class BackgroundJobServerOptions
96 | {
97 | public BackgroundJobServerOptions();
98 |
99 | public string ServerName { get; set; }
100 | public int WorkerCount { get; set; }
101 | public string[] Queues { get; set; }
102 | public TimeSpan ShutdownTimeout { get; set; }
103 | public TimeSpan SchedulePollingInterval { get; set; }
104 | public TimeSpan HeartbeatInterval { get; set; }
105 | public TimeSpan ServerTimeout { get; set; }
106 | public TimeSpan ServerCheckInterval { get; set; }
107 | public IJobFilterProvider FilterProvider { get; set; }
108 | public JobActivator Activator { get; set; }
109 | }
110 | }
111 | ```
112 |
113 | Of these options, several interval options may be manually set (to longer intervals) to reduce CPU load:
114 |
115 | - `SchedulePollingInterval` is by default set to [15 seconds](http://docs.hangfire.io/en/latest/background-methods/calling-methods-with-delay.html).
116 |
117 | - `HeartbeatInterval` is by default set to [30 seconds](https://github.com/HangfireIO/Hangfire/blob/master/src/Hangfire.Core/Server/ServerHeartbeat.cs).
118 |
119 | - `ServerTimeout` and `ServerCheckInterval` is by default set to [5 minutes](https://github.com/HangfireIO/Hangfire/blob/master/src/Hangfire.Core/Server/ServerWatchdog.cs).
120 |
121 | ### Dashboard
122 |
123 | Written below is a short snippet on how to implement Hangfire dashboard in ASP.NET Core MVC applications, with limited access to cookie-authenticated users of `Administrator` role. [Read more in official documentation](http://docs.hangfire.io/en/latest/configuration/using-dashboard.html).
124 |
125 | ```cs
126 | public class AdministratorHangfireDashboardAuthorizationFilter : IDashboardAuthorizationFilter
127 | {
128 | public bool Authorize(DashboardContext context)
129 | {
130 | var user = context.GetHttpContext().User;
131 | return user.Identity.IsAuthenticated && user.IsInRole("Administrator");
132 | }
133 | }
134 | ```
135 |
136 | ```cs
137 | public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
138 | {
139 | app.UseCookieAuthentication(...);
140 |
141 | // This middleware must be placed AFTER the authentication middlewares!
142 | app.UseHangfireDashboard(options: new DashboardOptions
143 | {
144 | Authorization = new[] { new AdministratorHangfireDashboardAuthorizationFilter() }
145 | });
146 | }
147 | ```
148 |
149 | ### Jobs via ASP.NET Core Dependency Injection Services
150 |
151 | For cleaner and more managable application code, it is possible to define your jobs in a class that is [registered via dependency injection](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection).
152 |
153 | ```cs
154 | public class MyHangfireJobs
155 | {
156 | public async Task SendGetRequest()
157 | {
158 | var client = new HttpClient();
159 | await client.GetAsync("https://www.accelist.com");
160 | }
161 | }
162 | ```
163 |
164 | ```cs
165 | public void ConfigureServices(IServiceCollection services)
166 | {
167 | services.AddTransient();
168 | }
169 | ```
170 |
171 | Using this technique, the registered jobs service will be able to obtain other services as dependency via constructor parameters, such as Entity Framework Core `DbContext`; which enables the development of powerful jobs with relative ease.
172 |
173 | Then later you can execute the jobs using generic expression:
174 |
175 | ```cs
176 | BackgroundJob.Enqueue(jobs => jobs.SendGetRequest());
177 |
178 | BackgroundJob.Schedule(jobs => jobs.SendGetRequest(), DateTimeOffset.UtcNow.AddDays(1));
179 |
180 | RecurringJob.AddOrUpdate("RecurringSendGetRequest", jobs => jobs.SendGetRequest(), Cron.Hourly());
181 | ```
182 |
--------------------------------------------------------------------------------
/Hangfire.Redis.StackExchange/RedisStorage.cs:
--------------------------------------------------------------------------------
1 | // Copyright © 2013-2015 Sergey Odinokov, Marco Casamento
2 | // This software is based on https://github.com/HangfireIO/Hangfire.Redis
3 |
4 | // Hangfire.Redis.StackExchange is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as
6 | // published by the Free Software Foundation, either version 3
7 | // of the License, or any later version.
8 | //
9 | // Hangfire.Redis.StackExchange is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public
15 | // License along with Hangfire.Redis.StackExchange. If not, see .
16 |
17 | using System;
18 | using System.Collections.Generic;
19 | using System.Linq;
20 | using Hangfire.Annotations;
21 | using Hangfire.Dashboard;
22 | using Hangfire.Logging;
23 | using Hangfire.Server;
24 | using Hangfire.States;
25 | using Hangfire.Storage;
26 | using StackExchange.Redis;
27 |
28 | namespace Hangfire.Redis.StackExchange
29 | {
30 | public class RedisStorage : JobStorage
31 | {
32 | // Make sure in Redis Cluster all transaction are in the same slot !!
33 | private readonly RedisStorageOptions _options;
34 | private readonly IConnectionMultiplexer _connectionMultiplexer;
35 | private readonly RedisSubscription _subscription;
36 | private readonly ConfigurationOptions _redisOptions;
37 |
38 | private readonly Dictionary _features =
39 | new Dictionary(StringComparer.OrdinalIgnoreCase)
40 | {
41 | { JobStorageFeatures.ExtendedApi, true },
42 | { JobStorageFeatures.JobQueueProperty, true },
43 | { JobStorageFeatures.Connection.BatchedGetFirstByLowest, true },
44 | { JobStorageFeatures.Connection.GetUtcDateTime, true },
45 | { JobStorageFeatures.Connection.GetSetContains, true },
46 | { JobStorageFeatures.Connection.LimitedGetSetCount, true },
47 | { JobStorageFeatures.Transaction.AcquireDistributedLock, true },
48 | { JobStorageFeatures.Transaction.CreateJob, true }, // overridden in constructor
49 | { JobStorageFeatures.Transaction.SetJobParameter, true}, // overridden in constructor
50 | { JobStorageFeatures.Transaction.RemoveFromQueue(typeof(RedisFetchedJob)), true }, // overridden in constructor
51 | { JobStorageFeatures.Monitoring.DeletedStateGraphs, true },
52 | { JobStorageFeatures.Monitoring.AwaitingJobs, true }
53 | };
54 |
55 | public RedisStorage()
56 | : this("localhost:6379", null, null)
57 | {
58 | }
59 |
60 | public RedisStorage(IConnectionMultiplexer connectionMultiplexer, RedisStorageOptions options = null)
61 | : this("UseConnectionMultiplexer", connectionMultiplexer, options)
62 | {
63 | }
64 |
65 | public RedisStorage(string connectionString, RedisStorageOptions options = null)
66 | : this(connectionString, null, options)
67 | {
68 | }
69 |
70 | private RedisStorage(string connectionString, IConnectionMultiplexer connectionMultiplexer,
71 | RedisStorageOptions options = null)
72 | {
73 | if (connectionString == null)
74 | throw new ArgumentNullException(nameof(connectionString));
75 | if (connectionString == "UseConnectionMultiplexer" && connectionMultiplexer == null)
76 | throw new ArgumentNullException(nameof(connectionMultiplexer));
77 |
78 | _connectionMultiplexer = connectionMultiplexer ?? ConnectionMultiplexer.Connect(connectionString);
79 |
80 | _redisOptions = ConfigurationOptions.Parse(_connectionMultiplexer.Configuration);
81 |
82 | _options = options ?? new RedisStorageOptions
83 | {
84 | Db = _redisOptions.DefaultDatabase ?? 0
85 | };
86 |
87 | SetTransactionalFeatures();
88 |
89 | _subscription = new RedisSubscription(this, _connectionMultiplexer.GetSubscriber());
90 | }
91 |
92 | private void SetTransactionalFeatures()
93 | {
94 | _features[JobStorageFeatures.Transaction.CreateJob] = _options.UseTransactions;
95 | _features[JobStorageFeatures.Transaction.SetJobParameter] = _options.UseTransactions;
96 | _features[JobStorageFeatures.Transaction.RemoveFromQueue(typeof(RedisFetchedJob))] = _options.UseTransactions;
97 | }
98 |
99 | public string ConnectionString => _connectionMultiplexer.Configuration;
100 |
101 | public int Db => _options.Db;
102 |
103 | internal int SucceededListSize => _options.SucceededListSize;
104 |
105 | internal int DeletedListSize => _options.DeletedListSize;
106 |
107 | internal RedisChannel SubscriptionChannel => _subscription.Channel;
108 |
109 | internal string[] LifoQueues => _options.LifoQueues;
110 |
111 | internal bool UseTransactions => _options.UseTransactions;
112 |
113 | public override IMonitoringApi GetMonitoringApi()
114 | {
115 | return new RedisMonitoringApi(this, _connectionMultiplexer.GetDatabase(Db));
116 | }
117 | public override bool HasFeature([NotNull] string featureId)
118 | {
119 | if (featureId == null) throw new ArgumentNullException(nameof(featureId));
120 |
121 | return _features.TryGetValue(featureId, out var isSupported)
122 | ? isSupported
123 | : base.HasFeature(featureId);
124 |
125 | }
126 | public override IStorageConnection GetConnection()
127 | {
128 | var endPoints = _connectionMultiplexer.GetEndPoints(false);
129 | IServer server = endPoints.Select(endPoint => _connectionMultiplexer.GetServer(endPoint))
130 | .FirstOrDefault(s => s.IsConnected && !s.IsReplica);
131 |
132 | if (server == null)
133 | throw new RedisConnectionException(ConnectionFailureType.UnableToConnect, "No redis server available");
134 |
135 | return new RedisConnection(this, server, _connectionMultiplexer.GetDatabase(Db), _subscription, _options.FetchTimeout);
136 | }
137 |
138 | #pragma warning disable 618
139 | public override IEnumerable GetComponents()
140 | #pragma warning restore 618
141 | {
142 | yield return new FetchedJobsWatcher(this, _options.InvisibilityTimeout);
143 | yield return new ExpiredJobsWatcher(this, _options.ExpiryCheckInterval);
144 | yield return _subscription;
145 | }
146 |
147 | public static DashboardMetric GetDashboardMetricFromRedisInfo(string title, string key)
148 | {
149 | return new DashboardMetric("redis:" + key, title, (razorPage) =>
150 | {
151 | using (var redisCnn = razorPage.Storage.GetConnection())
152 | {
153 | var db = (redisCnn as RedisConnection).Redis;
154 | var cnnMultiplexer = db.Multiplexer;
155 | var srv = cnnMultiplexer.GetServer(db.IdentifyEndpoint());
156 | var rawInfo = srv.InfoRaw().Split('\n')
157 | .Where(x => x.Contains(':'))
158 | .ToDictionary(x => x.Split(':')[0], x => x.Split(':')[1]);
159 |
160 | return new Metric(rawInfo[key]);
161 | }
162 | });
163 | }
164 |
165 | public override IEnumerable GetStateHandlers()
166 | {
167 | yield return new FailedStateHandler();
168 | yield return new ProcessingStateHandler();
169 | yield return new SucceededStateHandler();
170 | yield return new DeletedStateHandler();
171 | }
172 |
173 | public override void WriteOptionsToLog(ILog logger)
174 | {
175 | logger.Debug("Using the following options for Redis job storage:");
176 |
177 | var connectionString = _redisOptions.ToString(includePassword: false);
178 | logger.DebugFormat("ConnectionString: {0}\nDN: {1}", connectionString, Db);
179 | }
180 |
181 | public override string ToString()
182 | {
183 | var connectionString = _redisOptions.ToString(includePassword: false);
184 | return string.Format("redis://{0}/{1}", connectionString, Db);
185 | }
186 |
187 | internal string GetRedisKey([NotNull] string key)
188 | {
189 | if (key == null) throw new ArgumentNullException(nameof(key));
190 |
191 | return _options.Prefix + key;
192 | }
193 | }
194 | }
195 |
196 |
--------------------------------------------------------------------------------
/Hangfire.Redis.Tests/RedisConnectionFacts.cs:
--------------------------------------------------------------------------------
1 | using StackExchange.Redis;
2 | using System;
3 | using System.Collections.Generic;
4 | using Hangfire.Redis.StackExchange;
5 | using Hangfire.Redis.Tests.Utils;
6 | using Xunit;
7 |
8 | namespace Hangfire.Redis.Tests
9 | {
10 | [Collection("Sequential")]
11 | public class RedisConnectionFacts
12 | {
13 | private readonly RedisStorage _storage;
14 |
15 | public RedisConnectionFacts()
16 | {
17 | var options = new RedisStorageOptions() {Db = RedisUtils.GetDb()};
18 | _storage = new RedisStorage(RedisUtils.GetHostAndPort(), options);
19 | }
20 |
21 | [Fact, CleanRedis]
22 | public void GetStateData_ThrowsAnException_WhenJobIdIsNull()
23 | {
24 | UseConnection(
25 | connection => Assert.Throws("jobId",
26 | () => connection.GetStateData(null)));
27 | }
28 |
29 | [Fact, CleanRedis]
30 | public void GetStateData_ReturnsNull_WhenJobDoesNotExist()
31 | {
32 | UseConnection(connection =>
33 | {
34 | var result = connection.GetStateData("random-id");
35 | Assert.Null(result);
36 | });
37 | }
38 |
39 | [Fact, CleanRedis]
40 | public void GetStateData_ReturnsCorrectResult()
41 | {
42 | UseConnections((redis, connection) =>
43 | {
44 | redis.HashSet(
45 | "{hangfire}:job:my-job:state",
46 | new Dictionary
47 | {
48 | {"State", "Name"},
49 | {"Reason", "Reason"},
50 | {"Key", "Value"}
51 | }.ToHashEntries());
52 |
53 | var result = connection.GetStateData("my-job");
54 |
55 | Assert.NotNull(result);
56 | Assert.Equal("Name", result.Name);
57 | Assert.Equal("Reason", result.Reason);
58 | Assert.Equal("Value", result.Data["Key"]);
59 | });
60 | }
61 |
62 | [Fact, CleanRedis]
63 | public void GetStateData_ReturnsNullReason_IfThereIsNoSuchKey()
64 | {
65 | UseConnections((redis, connection) =>
66 | {
67 | redis.HashSet(
68 | "{hangfire}:job:my-job:state",
69 | new Dictionary
70 | {
71 | {"State", "Name"}
72 | }.ToHashEntries());
73 |
74 | var result = connection.GetStateData("my-job");
75 |
76 | Assert.NotNull(result);
77 | Assert.Null(result.Reason);
78 | });
79 | }
80 |
81 | [Fact, CleanRedis]
82 | public void GetAllItemsFromSet_ThrowsAnException_WhenKeyIsNull()
83 | {
84 | UseConnection(connection =>
85 | Assert.Throws("key",
86 | () => connection.GetAllItemsFromSet(null)));
87 | }
88 |
89 | [Fact, CleanRedis]
90 | public void GetAllItemsFromSet_ReturnsEmptyCollection_WhenSetDoesNotExist()
91 | {
92 | UseConnection(connection =>
93 | {
94 | var result = connection.GetAllItemsFromSet("some-set");
95 |
96 | Assert.NotNull(result);
97 | Assert.Empty(result);
98 | });
99 | }
100 |
101 | [Fact, CleanRedis]
102 | public void GetAllItemsFromSet_ReturnsAllItems()
103 | {
104 | UseConnections((redis, connection) =>
105 | {
106 | // Arrange
107 | redis.SortedSetAdd("{hangfire}:some-set", "1", 0);
108 | redis.SortedSetAdd("{hangfire}:some-set", "2", 0);
109 |
110 | // Act
111 | var result = connection.GetAllItemsFromSet("some-set");
112 |
113 | // Assert
114 | Assert.Equal(2, result.Count);
115 | Assert.Contains("1", result);
116 | Assert.Contains("2", result);
117 | });
118 | }
119 |
120 | [Fact, CleanRedis]
121 | public void SetRangeInHash_ThrowsAnException_WhenKeyIsNull()
122 | {
123 | UseConnection(connection =>
124 | {
125 | Assert.Throws("key",
126 | () => connection.SetRangeInHash(null, new Dictionary()));
127 | });
128 | }
129 |
130 | [Fact, CleanRedis]
131 | public void SetRangeInHash_ThrowsAnException_WhenKeyValuePairsArgumentIsNull()
132 | {
133 | UseConnection(connection =>
134 | {
135 | Assert.Throws("keyValuePairs",
136 | () => connection.SetRangeInHash("some-hash", null));
137 | });
138 | }
139 |
140 | [Fact, CleanRedis]
141 | public void SetRangeInHash_SetsAllGivenKeyPairs()
142 | {
143 | UseConnections((redis, connection) =>
144 | {
145 | connection.SetRangeInHash("some-hash", new Dictionary
146 | {
147 | {"Key1", "Value1"},
148 | {"Key2", "Value2"}
149 | });
150 |
151 | var hash = redis.HashGetAll("{hangfire}:some-hash").ToStringDictionary();
152 | Assert.Equal("Value1", hash["Key1"]);
153 | Assert.Equal("Value2", hash["Key2"]);
154 | });
155 | }
156 |
157 | [Fact, CleanRedis]
158 | public void GetAllEntriesFromHash_ThrowsAnException_WhenKeyIsNull()
159 | {
160 | UseConnection(connection =>
161 | Assert.Throws(() => connection.GetAllEntriesFromHash(null)));
162 | }
163 |
164 | [Fact, CleanRedis]
165 | public void GetAllEntriesFromHash_ReturnsNullValue_WhenHashDoesNotExist()
166 | {
167 | UseConnection(connection =>
168 | {
169 | var result = connection.GetAllEntriesFromHash("some-hash");
170 | Assert.Null(result);
171 | });
172 | }
173 |
174 | [Fact, CleanRedis]
175 | public void GetAllEntriesFromHash_ReturnsAllEntries()
176 | {
177 | UseConnections((redis, connection) =>
178 | {
179 | // Arrange
180 | redis.HashSet("{hangfire}:some-hash", new Dictionary
181 | {
182 | {"Key1", "Value1"},
183 | {"Key2", "Value2"}
184 | }.ToHashEntries());
185 |
186 | // Act
187 | var result = connection.GetAllEntriesFromHash("some-hash");
188 |
189 | // Assert
190 | Assert.NotNull(result);
191 | Assert.Equal("Value1", result["Key1"]);
192 | Assert.Equal("Value2", result["Key2"]);
193 | });
194 | }
195 |
196 | [Fact]
197 | public void GetUtcDateTime_ReturnsValidDateTime()
198 | {
199 | UseConnections((redis, connection) =>
200 | {
201 | var result = connection.GetUtcDateTime();
202 |
203 | Assert.NotEqual(default, result);
204 | });
205 | }
206 |
207 | [Fact]
208 | public void SetCount_ReturnZeroIfSetDoesNotExists()
209 | {
210 | UseConnections((redis, connection) =>
211 | {
212 | var result = connection.GetSetCount("some-set");
213 |
214 | Assert.Equal(0, result);
215 | });
216 | }
217 |
218 | [Fact, CleanRedis]
219 | public void SetCount_ReturnNumberOfItems()
220 | {
221 | UseConnections((redis, connection) =>
222 | {
223 | redis.SortedSetAdd("{hangfire}:some-set", "1", 0);
224 | redis.SortedSetAdd("{hangfire}:some-set", "2", 0);
225 |
226 | var result = connection.GetSetCount("some-set");
227 |
228 | Assert.Equal(2, result);
229 | });
230 | }
231 |
232 | [Fact, CleanRedis]
233 | public void SetContains_ReturnTrueIfContained()
234 | {
235 | UseConnections((redis, connection) =>
236 | {
237 | redis.SortedSetAdd("{hangfire}:some-set", "1", 0);
238 |
239 | var result = connection.GetSetContains("some-set", "1");
240 |
241 | Assert.True(result);
242 | });
243 | }
244 |
245 | [Fact, CleanRedis]
246 | public void SetContains_ReturnFalseIfNotContained()
247 | {
248 | UseConnections((redis, connection) =>
249 | {
250 | redis.SortedSetAdd("{hangfire}:some-set", "1", 0);
251 |
252 | var result = connection.GetSetContains("some-set", "0");
253 |
254 | Assert.False(result);
255 | });
256 | }
257 |
258 | private void UseConnections(Action action)
259 | {
260 | var redis = RedisUtils.CreateClient();
261 | var subscription = new RedisSubscription(_storage, RedisUtils.CreateSubscriber());
262 | var server = RedisUtils.GetFirstServer();
263 | using (var connection = new RedisConnection(_storage, server, redis, subscription, new RedisStorageOptions().FetchTimeout))
264 | {
265 | action(redis, connection);
266 | }
267 | }
268 |
269 | private void UseConnection(Action action)
270 | {
271 | var redis = RedisUtils.CreateClient();
272 | var subscription = new RedisSubscription(_storage, RedisUtils.CreateSubscriber());
273 | var server = RedisUtils.GetFirstServer();
274 |
275 | using (var connection = new RedisConnection(_storage, server, redis, subscription, new RedisStorageOptions().FetchTimeout))
276 | {
277 | action(connection);
278 | }
279 | }
280 |
281 | }
282 | }
--------------------------------------------------------------------------------
/Hangfire.Redis.StackExchange/RedisWriteDirectlyToDatabase.cs:
--------------------------------------------------------------------------------
1 | // Copyright © 2013-2015 Sergey Odinokov, Marco Casamento
2 | // This software is based on https://github.com/HangfireIO/Hangfire.Redis
3 |
4 | // Hangfire.Redis.StackExchange is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as
6 | // published by the Free Software Foundation, either version 3
7 | // of the License, or any later version.
8 | //
9 | // Hangfire.Redis.StackExchange is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public
15 | // License along with Hangfire.Redis.StackExchange. If not, see .
16 |
17 | using System;
18 | using System.Collections.Generic;
19 | using System.Linq;
20 | using System.Threading.Tasks;
21 | using Hangfire.Annotations;
22 | using Hangfire.Common;
23 | using Hangfire.States;
24 | using Hangfire.Storage;
25 | using StackExchange.Redis;
26 |
27 | namespace Hangfire.Redis.StackExchange
28 | {
29 | internal class RedisWriteDirectlyToDatabase : JobStorageTransaction
30 | {
31 | private readonly RedisStorage _storage;
32 | private readonly IDatabase _database;
33 |
34 | public RedisWriteDirectlyToDatabase([NotNull] RedisStorage storage, [NotNull] IDatabase database)
35 | {
36 | _storage = storage ?? throw new ArgumentNullException(nameof(storage));
37 | _database = database ?? throw new ArgumentNullException(nameof(database));
38 | }
39 |
40 | public override void AddRangeToSet([NotNull] string key, [NotNull] IList items)
41 | {
42 | _database.SortedSetAdd(_storage.GetRedisKey(key), items.Select(x => new SortedSetEntry(x, 0)).ToArray());
43 | }
44 |
45 | public override void ExpireHash([NotNull] string key, TimeSpan expireIn)
46 | {
47 | _database.KeyExpire(_storage.GetRedisKey(key), expireIn);
48 | }
49 |
50 | public override void ExpireList([NotNull] string key, TimeSpan expireIn)
51 | {
52 | _database.KeyExpire(_storage.GetRedisKey(key), expireIn);
53 | }
54 |
55 | public override void ExpireSet([NotNull] string key, TimeSpan expireIn)
56 | {
57 | _database.KeyExpire(_storage.GetRedisKey(key), expireIn);
58 | }
59 |
60 | public override void PersistHash([NotNull] string key)
61 | {
62 | _database.KeyPersist(_storage.GetRedisKey(key));
63 | }
64 |
65 | public override void PersistList([NotNull] string key)
66 | {
67 | _database.KeyPersist(_storage.GetRedisKey(key));
68 | }
69 |
70 | public override void PersistSet([NotNull] string key)
71 | {
72 | _database.KeyPersist(_storage.GetRedisKey(key));
73 | }
74 |
75 | public override void RemoveSet([NotNull] string key)
76 | {
77 | _database.KeyDelete(_storage.GetRedisKey(key));
78 | }
79 |
80 | public override void Commit()
81 | {
82 | //nothing to be done
83 | }
84 |
85 | public override void ExpireJob([NotNull] string jobId, TimeSpan expireIn)
86 | {
87 | if (jobId == null) throw new ArgumentNullException(nameof(jobId));
88 |
89 | var tasks = new Task[3];
90 |
91 | tasks[0] = _database.KeyExpireAsync(_storage.GetRedisKey($"job:{jobId}"), expireIn);
92 | tasks[1] = _database.KeyExpireAsync(_storage.GetRedisKey($"job:{jobId}:history"), expireIn);
93 | tasks[2] = _database.KeyExpireAsync(_storage.GetRedisKey($"job:{jobId}:state"), expireIn);
94 |
95 | Task.WaitAll(tasks);
96 | }
97 |
98 | public override void PersistJob([NotNull] string jobId)
99 | {
100 | if (jobId == null) throw new ArgumentNullException(nameof(jobId));
101 |
102 | var tasks = new Task[3];
103 |
104 | tasks[0] = _database.KeyPersistAsync(_storage.GetRedisKey($"job:{jobId}"));
105 | tasks[1] = _database.KeyPersistAsync(_storage.GetRedisKey($"job:{jobId}:history"));
106 | tasks[2] = _database.KeyPersistAsync(_storage.GetRedisKey($"job:{jobId}:state"));
107 |
108 | Task.WaitAll(tasks);
109 | }
110 |
111 | public override void SetJobState([NotNull] string jobId, [NotNull] IState state)
112 | {
113 | if (jobId == null) throw new ArgumentNullException(nameof(jobId));
114 | if (state == null) throw new ArgumentNullException(nameof(state));
115 |
116 | var tasks = new Task[3];
117 |
118 | tasks[0] = _database.HashSetAsync(_storage.GetRedisKey($"job:{jobId}"), "State", state.Name);
119 | tasks[1] = _database.KeyDeleteAsync(_storage.GetRedisKey($"job:{jobId}:state"));
120 |
121 | var storedData = new Dictionary(state.SerializeData())
122 | {
123 | {"State", state.Name}
124 | };
125 |
126 | if (state.Reason != null)
127 | storedData.Add("Reason", state.Reason);
128 |
129 | tasks[2] = _database.HashSetAsync(_storage.GetRedisKey($"job:{jobId}:state"), storedData.ToHashEntries());
130 |
131 | AddJobState(jobId, state);
132 |
133 | Task.WaitAll(tasks);
134 | }
135 |
136 | public override void AddJobState([NotNull] string jobId, [NotNull] IState state)
137 | {
138 | if (jobId == null) throw new ArgumentNullException(nameof(jobId));
139 | if (state == null) throw new ArgumentNullException(nameof(state));
140 |
141 | var storedData = new Dictionary(state.SerializeData())
142 | {
143 | {"State", state.Name},
144 | {"Reason", state.Reason},
145 | {"CreatedAt", JobHelper.SerializeDateTime(DateTime.UtcNow)}
146 | };
147 |
148 | _database.ListRightPush(
149 | _storage.GetRedisKey($"job:{jobId}:history"),
150 | SerializationHelper.Serialize(storedData));
151 | }
152 |
153 | public override void AddToQueue([NotNull] string queue, [NotNull] string jobId)
154 | {
155 | if (queue == null) throw new ArgumentNullException(nameof(queue));
156 | if (jobId == null) throw new ArgumentNullException(nameof(jobId));
157 |
158 | var tasks = new Task[3];
159 |
160 | tasks[0] = _database.SetAddAsync(_storage.GetRedisKey("queues"), queue);
161 | if (_storage.LifoQueues != null && _storage.LifoQueues.Contains(queue, StringComparer.OrdinalIgnoreCase))
162 | {
163 | tasks[1] = _database.ListRightPushAsync(_storage.GetRedisKey($"queue:{queue}"), jobId);
164 | }
165 | else
166 | {
167 | tasks[1] = _database.ListLeftPushAsync(_storage.GetRedisKey($"queue:{queue}"), jobId);
168 | }
169 |
170 | tasks[2] = _database.PublishAsync(_storage.SubscriptionChannel, jobId);
171 |
172 | Task.WaitAll(tasks);
173 | }
174 |
175 | public override void IncrementCounter([NotNull] string key)
176 | {
177 | _database.StringIncrement(_storage.GetRedisKey(key));
178 | }
179 |
180 | public override void IncrementCounter([NotNull] string key, TimeSpan expireIn)
181 | {
182 | var tasks = new Task[2];
183 |
184 | tasks[0] = _database.StringIncrementAsync(_storage.GetRedisKey(key));
185 | tasks[1] = _database.KeyExpireAsync(_storage.GetRedisKey(key), expireIn);
186 |
187 | Task.WaitAll(tasks);
188 | }
189 |
190 | public override void DecrementCounter([NotNull] string key)
191 | {
192 | _database.StringDecrement(_storage.GetRedisKey(key));
193 | }
194 |
195 | public override void DecrementCounter([NotNull] string key, TimeSpan expireIn)
196 | {
197 | var tasks = new Task[2];
198 |
199 | tasks[0] = _database.StringDecrementAsync(_storage.GetRedisKey(key));
200 | tasks[1] = _database.KeyExpireAsync(_storage.GetRedisKey(key), expireIn);
201 |
202 | Task.WaitAll(tasks);
203 | }
204 |
205 | public override void AddToSet([NotNull] string key, [NotNull] string value)
206 | {
207 | AddToSet(key, value, 0);
208 | }
209 |
210 | public override void AddToSet([NotNull] string key, [NotNull] string value, double score)
211 | {
212 | if (value == null) throw new ArgumentNullException(nameof(value));
213 |
214 | _database.SortedSetAdd(_storage.GetRedisKey(key), value, score);
215 | }
216 |
217 | public override void RemoveFromSet([NotNull] string key, [NotNull] string value)
218 | {
219 | if (value == null) throw new ArgumentNullException(nameof(value));
220 |
221 | _database.SortedSetRemove(_storage.GetRedisKey(key), value);
222 | }
223 |
224 | public override void InsertToList([NotNull] string key, string value)
225 | {
226 | _database.ListLeftPush(_storage.GetRedisKey(key), value);
227 | }
228 |
229 | public override void RemoveFromList([NotNull] string key, string value)
230 | {
231 | _database.ListRemove(_storage.GetRedisKey(key), value);
232 | }
233 |
234 | public override void TrimList([NotNull] string key, int keepStartingFrom, int keepEndingAt)
235 | {
236 | _database.ListTrim(_storage.GetRedisKey(key), keepStartingFrom, keepEndingAt);
237 | }
238 |
239 | public override void SetRangeInHash(
240 | [NotNull] string key, [NotNull] IEnumerable> keyValuePairs)
241 | {
242 | if (keyValuePairs == null) throw new ArgumentNullException(nameof(keyValuePairs));
243 |
244 | _database.HashSet(_storage.GetRedisKey(key), keyValuePairs.ToHashEntries());
245 | }
246 |
247 | public override void RemoveHash([NotNull] string key)
248 | {
249 | _database.KeyDelete(_storage.GetRedisKey(key));
250 | }
251 |
252 | public override void Dispose()
253 | {
254 | //Don't have to dispose anything
255 | }
256 | }
257 | }
--------------------------------------------------------------------------------
/Hangfire.Redis.Tests/RedisFetchedJobFacts.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Hangfire.Common;
3 | using Hangfire.Redis.StackExchange;
4 | using Hangfire.Redis.Tests.Utils;
5 | using Moq;
6 | using Xunit;
7 | using StackExchange.Redis;
8 |
9 | namespace Hangfire.Redis.Tests
10 | {
11 | [Collection("Sequential")]
12 | public class RedisFetchedJobFacts
13 | {
14 | private const string JobId = "id";
15 | private const string Queue = "queue";
16 |
17 | private readonly RedisStorage _storage;
18 | private readonly Mock _redis;
19 |
20 | public RedisFetchedJobFacts()
21 | {
22 | _redis = new Mock();
23 |
24 | var options = new RedisStorageOptions() { Db = RedisUtils.GetDb() };
25 | _storage = new RedisStorage(RedisUtils.GetHostAndPort(), options);
26 | }
27 |
28 | [Fact]
29 | public void Ctor_ThrowsAnException_WhenStorageIsNull()
30 | {
31 | Assert.Throws("storage",
32 | () => new RedisFetchedJob(null, _redis.Object, JobId, Queue, DateTime.UtcNow));
33 | }
34 |
35 | [Fact]
36 | public void Ctor_ThrowsAnException_WhenRedisIsNull()
37 | {
38 | Assert.Throws("redis",
39 | () => new RedisFetchedJob(_storage, null, JobId, Queue, DateTime.UtcNow));
40 | }
41 |
42 | [Fact]
43 | public void Ctor_ThrowsAnException_WhenJobIdIsNull()
44 | {
45 | Assert.Throws("jobId",
46 | () => new RedisFetchedJob(_storage, _redis.Object, null, Queue, DateTime.UtcNow));
47 | }
48 |
49 | [Fact]
50 | public void Ctor_ThrowsAnException_WhenQueueIsNull()
51 | {
52 | Assert.Throws("queue",
53 | () => new RedisFetchedJob(_storage, _redis.Object, JobId, null, DateTime.UtcNow));
54 | }
55 |
56 | [Fact, CleanRedis]
57 | public void RemoveFromQueue_RemovesJobFromTheFetchedList()
58 | {
59 | UseRedis(redis =>
60 | {
61 | // Arrange
62 | redis.ListRightPush("{hangfire}:queue:my-queue:dequeued", "job-id");
63 |
64 | var fetchedAt = DateTime.UtcNow;
65 | redis.HashSet("{hangfire}:job:job-id", "Fetched", JobHelper.SerializeDateTime(fetchedAt));
66 | var fetchedJob = new RedisFetchedJob(_storage, redis, "job-id", "my-queue", fetchedAt);
67 |
68 | // Act
69 | fetchedJob.RemoveFromQueue();
70 |
71 | // Assert
72 | Assert.Equal(0, redis.ListLength("{hangfire}:queue:my-queue:dequeued"));
73 | });
74 | }
75 |
76 | [Fact, CleanRedis]
77 | public void RemoveFromQueue_RemovesOnlyJobWithTheSpecifiedId()
78 | {
79 | UseRedis(redis =>
80 | {
81 | // Arrange
82 | redis.ListRightPush("{hangfire}:queue:my-queue:dequeued", "job-id");
83 | redis.ListRightPush("{hangfire}:queue:my-queue:dequeued", "another-job-id");
84 |
85 | var fetchedAt = DateTime.UtcNow;
86 | redis.HashSet("{hangfire}:job:job-id", "Fetched", JobHelper.SerializeDateTime(fetchedAt));
87 | redis.HashSet("{hangfire}:job:another-job-id", "Fetched", JobHelper.SerializeDateTime(fetchedAt));
88 | var fetchedJob = new RedisFetchedJob(_storage, redis, "job-id", "my-queue", fetchedAt);
89 |
90 | // Act
91 | fetchedJob.RemoveFromQueue();
92 |
93 | // Assert
94 | Assert.Equal(1, redis.ListLength("{hangfire}:queue:my-queue:dequeued"));
95 | Assert.Equal("another-job-id", (string)redis.ListRightPop("{hangfire}:queue:my-queue:dequeued"));
96 | });
97 | }
98 |
99 | [Fact, CleanRedis]
100 | public void RemoveFromQueue_DoesNotRemoveIfFetchedDoesntMatch()
101 | {
102 | UseRedis(redis =>
103 | {
104 | // Arrange
105 | redis.ListRightPush("{hangfire}:queue:my-queue:dequeued", "job-id");
106 |
107 | var fetchedAt = DateTime.UtcNow;
108 | redis.HashSet("{hangfire}:job:job-id", "Fetched", JobHelper.SerializeDateTime(fetchedAt));
109 | var fetchedJob = new RedisFetchedJob(_storage, redis, "job-id", "my-queue", fetchedAt + TimeSpan.FromSeconds(1));
110 |
111 | // Act
112 | fetchedJob.RemoveFromQueue();
113 |
114 | // Assert
115 | Assert.Equal(1, redis.ListLength("{hangfire}:queue:my-queue:dequeued"));
116 | Assert.Equal("job-id", (string)redis.ListRightPop("{hangfire}:queue:my-queue:dequeued"));
117 | });
118 | }
119 |
120 | [Fact, CleanRedis]
121 | public void RemoveFromQueue_RemovesTheFetchedFlag()
122 | {
123 | UseRedis(redis =>
124 | {
125 | // Arrange
126 | var fetchedAt = DateTime.UtcNow;
127 | redis.HashSet("{hangfire}:job:my-job", "Fetched", JobHelper.SerializeDateTime(fetchedAt));
128 | var fetchedJob = new RedisFetchedJob(_storage, redis, "my-job", "my-queue", fetchedAt);
129 |
130 | // Act
131 | fetchedJob.RemoveFromQueue();
132 |
133 | // Assert
134 | Assert.False(redis.HashExists("{hangfire}:job:my-job", "Fetched"));
135 | });
136 | }
137 |
138 | [Fact, CleanRedis]
139 | public void RemoveFromQueue_RemovesTheCheckedFlag()
140 | {
141 | UseRedis(redis =>
142 | {
143 | // Arrange
144 | redis.HashSet("{hangfire}:job:my-job", "Checked", JobHelper.SerializeDateTime(DateTime.UtcNow));
145 | var fetchedJob = new RedisFetchedJob(_storage, redis, "my-job", "my-queue", null);
146 |
147 | // Act
148 | fetchedJob.RemoveFromQueue();
149 |
150 | // Assert
151 | Assert.False(redis.HashExists("{hangfire}:job:my-job", "Checked"));
152 | });
153 | }
154 |
155 | [Fact, CleanRedis]
156 | public void Requeue_PushesAJobBackToQueue()
157 | {
158 | UseRedis(redis =>
159 | {
160 | // Arrange
161 | redis.ListRightPush("{hangfire}:queue:my-queue:dequeued", "my-job");
162 | var fetchedAt = DateTime.UtcNow;
163 | redis.HashSet("{hangfire}:job:my-job", "Fetched", JobHelper.SerializeDateTime(fetchedAt));
164 |
165 | var fetchedJob = new RedisFetchedJob(_storage, redis, "my-job", "my-queue", fetchedAt);
166 |
167 | // Act
168 | fetchedJob.Requeue();
169 |
170 | // Assert
171 | Assert.Equal("my-job", (string)redis.ListRightPop("{hangfire}:queue:my-queue"));
172 | Assert.Null((string)redis.ListLeftPop("{hangfire}:queue:my-queue:dequeued"));
173 | });
174 | }
175 |
176 | [Fact, CleanRedis]
177 | public void Requeue_PushesAJobToTheRightSide()
178 | {
179 | UseRedis(redis =>
180 | {
181 | // Arrange
182 | redis.ListRightPush("{hangfire}:queue:my-queue", "another-job");
183 | redis.ListRightPush("{hangfire}:queue:my-queue:dequeued", "my-job");
184 | var fetchedAt = DateTime.UtcNow;
185 | redis.HashSet("{hangfire}:job:my-job", "Fetched", JobHelper.SerializeDateTime(fetchedAt));
186 |
187 |
188 | var fetchedJob = new RedisFetchedJob(_storage, redis, "my-job", "my-queue", fetchedAt);
189 |
190 | // Act
191 | fetchedJob.Requeue();
192 |
193 | // Assert - RPOP
194 | Assert.Equal("my-job", (string)redis.ListRightPop("{hangfire}:queue:my-queue"));
195 | Assert.Null((string)redis.ListRightPop("{hangfire}:queue:my-queue:dequeued"));
196 | });
197 | }
198 |
199 | [Fact, CleanRedis]
200 | public void Requeue_RemovesAJobFromFetchedList()
201 | {
202 | UseRedis(redis =>
203 | {
204 | // Arrange
205 | redis.ListRightPush("{hangfire}:queue:my-queue:dequeued", "my-job");
206 | var fetchedAt = DateTime.UtcNow;
207 | redis.HashSet("{hangfire}:job:my-job", "Fetched", JobHelper.SerializeDateTime(fetchedAt));
208 |
209 | var fetchedJob = new RedisFetchedJob(_storage, redis, "my-job", "my-queue", fetchedAt);
210 |
211 | // Act
212 | fetchedJob.Requeue();
213 |
214 | // Assert
215 | Assert.Equal(0, redis.ListLength("{hangfire}:queue:my-queue:dequeued"));
216 | });
217 | }
218 |
219 | [Fact, CleanRedis]
220 | public void Requeue_RemovesTheFetchedFlag()
221 | {
222 | UseRedis(redis =>
223 | {
224 | // Arrange
225 | var fetchedAt = DateTime.UtcNow;
226 | redis.HashSet("{hangfire}:job:my-job", "Fetched", JobHelper.SerializeDateTime(fetchedAt));
227 | var fetchedJob = new RedisFetchedJob(_storage, redis, "my-job", "my-queue", fetchedAt);
228 |
229 | // Act
230 | fetchedJob.Requeue();
231 |
232 | // Assert
233 | Assert.False(redis.HashExists("{hangfire}:job:my-job", "Fetched"));
234 | });
235 | }
236 |
237 | [Fact, CleanRedis]
238 | public void Requeue_RemovesTheCheckedFlag()
239 | {
240 | UseRedis(redis =>
241 | {
242 | // Arrange
243 | redis.HashSet("{hangfire}:job:my-job", "Checked", JobHelper.SerializeDateTime(DateTime.UtcNow));
244 | var fetchedJob = new RedisFetchedJob(_storage, redis, "my-job", "my-queue", null);
245 |
246 | // Act
247 | fetchedJob.Requeue();
248 |
249 | // Assert
250 | Assert.False(redis.HashExists("{hangfire}:job:my-job", "Checked"));
251 | });
252 | }
253 |
254 | [Fact, CleanRedis]
255 | public void Dispose_WithNoComplete_RequeuesAJob()
256 | {
257 | UseRedis(redis =>
258 | {
259 | // Arrange
260 | redis.ListRightPush("{hangfire}:queue:my-queue:dequeued", "my-job");
261 | var fetchedJob = new RedisFetchedJob(_storage, redis, "my-job", "my-queue", DateTime.UtcNow);
262 |
263 | // Act
264 | fetchedJob.Dispose();
265 |
266 | // Assert
267 | Assert.Equal(1, redis.ListLength("{hangfire}:queue:my-queue"));
268 | });
269 | }
270 |
271 | [Fact, CleanRedis]
272 | public void Dispose_AfterRemoveFromQueue_DoesNotRequeueAJob()
273 | {
274 | UseRedis(redis =>
275 | {
276 | // Arrange
277 | redis.ListRightPush("{hangfire}:queue:my-queue:dequeued", "my-job");
278 | redis.ListRightPush("{hangfire}:queue:my-queue:dequeued", "my-job");
279 | var fetchedJob = new RedisFetchedJob(_storage, redis, "my-job", "my-queue", DateTime.UtcNow);
280 |
281 | // Act
282 | fetchedJob.RemoveFromQueue();
283 | fetchedJob.Dispose();
284 |
285 | // Assert
286 | Assert.Equal(0, redis.ListLength("{hangfire}:queue:my-queue"));
287 | });
288 | }
289 |
290 | private static void UseRedis(Action action)
291 | {
292 | var redis = RedisUtils.CreateClient();
293 | action(redis);
294 | }
295 | }
296 | }
297 |
--------------------------------------------------------------------------------
/Hangfire.Redis.StackExchange/RedisWriteOnlyTransaction.cs:
--------------------------------------------------------------------------------
1 | // Copyright © 2013-2015 Sergey Odinokov, Marco Casamento
2 | // This software is based on https://github.com/HangfireIO/Hangfire.Redis
3 |
4 | // Hangfire.Redis.StackExchange is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as
6 | // published by the Free Software Foundation, either version 3
7 | // of the License, or any later version.
8 | //
9 | // Hangfire.Redis.StackExchange is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public
15 | // License along with Hangfire.Redis.StackExchange. If not, see .
16 |
17 | using System;
18 | using System.Collections.Generic;
19 | using System.Linq;
20 | using Hangfire.Annotations;
21 | using Hangfire.Common;
22 | using Hangfire.States;
23 | using Hangfire.Storage;
24 | using StackExchange.Redis;
25 |
26 | namespace Hangfire.Redis.StackExchange
27 | {
28 | internal class RedisWriteOnlyTransaction : JobStorageTransaction
29 | {
30 | private readonly RedisStorage _storage;
31 | private readonly ITransaction _transaction;
32 | private readonly List _lockToDispose = new List();
33 | public RedisWriteOnlyTransaction([NotNull] RedisStorage storage, [NotNull] ITransaction transaction)
34 | {
35 | if (storage == null) throw new ArgumentNullException(nameof(storage));
36 | if (transaction == null) throw new ArgumentNullException(nameof(transaction));
37 |
38 | _storage = storage;
39 | _transaction = transaction;
40 | }
41 |
42 | public override void AddRangeToSet([NotNull] string key, [NotNull] IList items)
43 | {
44 | _transaction.SortedSetAddAsync(_storage.GetRedisKey(key), items.Select(x => new SortedSetEntry(x, 0)).ToArray());
45 | }
46 | public override void AcquireDistributedLock([NotNull] string resource, TimeSpan timeout)
47 | {
48 | var distributedLock = _storage.GetConnection().AcquireDistributedLock(resource, timeout);
49 | _lockToDispose.Add(distributedLock);
50 | }
51 | public override void ExpireHash([NotNull] string key, TimeSpan expireIn)
52 | {
53 | _transaction.KeyExpireAsync(_storage.GetRedisKey(key), expireIn);
54 | }
55 |
56 | public override void ExpireList([NotNull] string key, TimeSpan expireIn)
57 | {
58 | _transaction.KeyExpireAsync(_storage.GetRedisKey(key), expireIn);
59 | }
60 |
61 | public override void ExpireSet([NotNull] string key, TimeSpan expireIn)
62 | {
63 | _transaction.KeyExpireAsync(_storage.GetRedisKey(key), expireIn);
64 | }
65 |
66 | public override void PersistHash([NotNull] string key)
67 | {
68 | _transaction.KeyPersistAsync(_storage.GetRedisKey(key));
69 | }
70 |
71 | public override void PersistList([NotNull] string key)
72 | {
73 | _transaction.KeyPersistAsync(_storage.GetRedisKey(key));
74 | }
75 |
76 | public override void PersistSet([NotNull] string key)
77 | {
78 | _transaction.KeyPersistAsync(_storage.GetRedisKey(key));
79 | }
80 |
81 | public override void RemoveSet([NotNull] string key)
82 | {
83 | _transaction.KeyDeleteAsync(_storage.GetRedisKey(key));
84 | }
85 |
86 | public override void Commit()
87 | {
88 | if (!_transaction.Execute())
89 | {
90 | // RedisTransaction.Commit returns false only when
91 | // WATCH condition has been failed. So, we should
92 | // re-play the transaction.
93 |
94 | int replayCount = 1;
95 | const int maxReplayCount = 3;
96 | while (!_transaction.Execute())
97 | {
98 | if (replayCount++ >= maxReplayCount)
99 | {
100 | throw new HangFireRedisException("Transaction commit was failed due to WATCH condition failure. Retry attempts exceeded.");
101 | }
102 | }
103 | }
104 | }
105 |
106 | public override void ExpireJob([NotNull] string jobId, TimeSpan expireIn)
107 | {
108 | if (jobId == null) throw new ArgumentNullException(nameof(jobId));
109 |
110 | _transaction.KeyExpireAsync(_storage.GetRedisKey($"job:{jobId}"), expireIn);
111 | _transaction.KeyExpireAsync(_storage.GetRedisKey($"job:{jobId}:history"), expireIn);
112 | _transaction.KeyExpireAsync(_storage.GetRedisKey($"job:{jobId}:state"), expireIn);
113 | }
114 |
115 | public override void PersistJob([NotNull] string jobId)
116 | {
117 | if (jobId == null) throw new ArgumentNullException(nameof(jobId));
118 |
119 | _transaction.KeyPersistAsync(_storage.GetRedisKey($"job:{jobId}"));
120 | _transaction.KeyPersistAsync(_storage.GetRedisKey($"job:{jobId}:history"));
121 | _transaction.KeyPersistAsync(_storage.GetRedisKey($"job:{jobId}:state"));
122 | }
123 |
124 | public override void SetJobState([NotNull] string jobId, [NotNull] IState state)
125 | {
126 | if (jobId == null) throw new ArgumentNullException(nameof(jobId));
127 | if (state == null) throw new ArgumentNullException(nameof(state));
128 |
129 | _transaction.HashSetAsync(_storage.GetRedisKey($"job:{jobId}"), "State", state.Name);
130 | _transaction.KeyDeleteAsync(_storage.GetRedisKey($"job:{jobId}:state"));
131 |
132 | var storedData = new Dictionary(state.SerializeData())
133 | {
134 | {"State", state.Name}
135 | };
136 |
137 | if (state.Reason != null)
138 | storedData.Add("Reason", state.Reason);
139 |
140 | _transaction.HashSetAsync(_storage.GetRedisKey($"job:{jobId}:state"), storedData.ToHashEntries());
141 |
142 | AddJobState(jobId, state);
143 | }
144 |
145 | public override void AddJobState([NotNull] string jobId, [NotNull] IState state)
146 | {
147 | if (jobId == null) throw new ArgumentNullException(nameof(jobId));
148 | if (state == null) throw new ArgumentNullException(nameof(state));
149 |
150 | var storedData = new Dictionary(state.SerializeData())
151 | {
152 | {"State", state.Name},
153 | {"Reason", state.Reason},
154 | {"CreatedAt", JobHelper.SerializeDateTime(DateTime.UtcNow)}
155 | };
156 |
157 | _transaction.ListRightPushAsync(
158 | _storage.GetRedisKey($"job:{jobId}:history"),
159 | SerializationHelper.Serialize(storedData));
160 | }
161 |
162 | public override void AddToQueue([NotNull] string queue, [NotNull] string jobId)
163 | {
164 | if (queue == null) throw new ArgumentNullException(nameof(queue));
165 | if (jobId == null) throw new ArgumentNullException(nameof(jobId));
166 |
167 | _transaction.SetAddAsync(_storage.GetRedisKey("queues"), queue);
168 | if (_storage.LifoQueues != null && _storage.LifoQueues.Contains(queue, StringComparer.OrdinalIgnoreCase))
169 | {
170 | _transaction.ListRightPushAsync(_storage.GetRedisKey($"queue:{queue}"), jobId);
171 | }
172 | else
173 | {
174 | _transaction.ListLeftPushAsync(_storage.GetRedisKey($"queue:{queue}"), jobId);
175 | }
176 |
177 | _transaction.PublishAsync(_storage.SubscriptionChannel, jobId);
178 | }
179 |
180 | public override void IncrementCounter([NotNull] string key)
181 | {
182 | _transaction.StringIncrementAsync(_storage.GetRedisKey(key));
183 | }
184 |
185 | public override void IncrementCounter([NotNull] string key, TimeSpan expireIn)
186 | {
187 | _transaction.StringIncrementAsync(_storage.GetRedisKey(key));
188 | _transaction.KeyExpireAsync(_storage.GetRedisKey(key), expireIn);
189 | }
190 |
191 | public override void DecrementCounter([NotNull] string key)
192 | {
193 | _transaction.StringDecrementAsync(_storage.GetRedisKey(key));
194 | }
195 |
196 | public override void DecrementCounter([NotNull] string key, TimeSpan expireIn)
197 | {
198 | _transaction.StringDecrementAsync(_storage.GetRedisKey(key));
199 | _transaction.KeyExpireAsync(_storage.GetRedisKey(key), expireIn);
200 | }
201 |
202 | public override void AddToSet([NotNull] string key, [NotNull] string value)
203 | {
204 | AddToSet(key, value, 0);
205 | }
206 |
207 | public override void AddToSet([NotNull] string key, [NotNull] string value, double score)
208 | {
209 | if (value == null) throw new ArgumentNullException(nameof(value));
210 |
211 | _transaction.SortedSetAddAsync(_storage.GetRedisKey(key), value, score);
212 | }
213 | public override string CreateJob([NotNull] Job job, [NotNull] IDictionary parameters, DateTime createdAt, TimeSpan expireIn)
214 | {
215 | if (job == null) throw new ArgumentNullException(nameof(job));
216 | if (parameters == null) throw new ArgumentNullException(nameof(parameters));
217 |
218 | var jobId = Guid.NewGuid().ToString();
219 |
220 | var invocationData = InvocationData.SerializeJob(job);
221 |
222 | // Do not modify the original parameters.
223 | var storedParameters = new Dictionary(parameters)
224 | {
225 | { "Queue", invocationData.Queue },
226 | { "Type", invocationData.Type },
227 | { "Method", invocationData.Method },
228 | { "ParameterTypes", invocationData.ParameterTypes },
229 | { "Arguments", invocationData.Arguments },
230 | { "CreatedAt", JobHelper.SerializeDateTime(createdAt) }
231 | };
232 | _ = _transaction.HashSetAsync(_storage.GetRedisKey($"job:{jobId}"), storedParameters.ToHashEntries());
233 | _ = _transaction.KeyExpireAsync(_storage.GetRedisKey($"job:{jobId}"), expireIn);
234 |
235 | if (! _transaction.Execute())
236 | throw new HangfireRedisTransactionException("Transaction Execution failure");
237 | return jobId;
238 | }
239 | public override void RemoveFromSet([NotNull] string key, [NotNull] string value)
240 | {
241 | if (value == null) throw new ArgumentNullException(nameof(value));
242 |
243 | _transaction.SortedSetRemoveAsync(_storage.GetRedisKey(key), value);
244 | }
245 |
246 | public override void InsertToList([NotNull] string key, string value)
247 | {
248 | _transaction.ListLeftPushAsync(_storage.GetRedisKey(key), value);
249 | }
250 |
251 | public override void RemoveFromList([NotNull] string key, string value)
252 | {
253 | _transaction.ListRemoveAsync(_storage.GetRedisKey(key), value);
254 | }
255 |
256 | public override void TrimList([NotNull] string key, int keepStartingFrom, int keepEndingAt)
257 | {
258 | _transaction.ListTrimAsync(_storage.GetRedisKey(key), keepStartingFrom, keepEndingAt);
259 | }
260 |
261 | public override void SetRangeInHash(
262 | [NotNull] string key, [NotNull] IEnumerable> keyValuePairs)
263 | {
264 | if (keyValuePairs == null) throw new ArgumentNullException(nameof(keyValuePairs));
265 |
266 | _transaction.HashSetAsync(_storage.GetRedisKey(key), keyValuePairs.ToHashEntries());
267 | }
268 | public override void SetJobParameter([NotNull] string jobId, [NotNull] string name, [CanBeNull] string value)
269 | {
270 | _storage.GetConnection().SetJobParameter(jobId, name, value);
271 | }
272 | public override void RemoveHash([NotNull] string key)
273 | {
274 | _transaction.KeyDeleteAsync(_storage.GetRedisKey(key));
275 | }
276 |
277 | public override void RemoveFromQueue([NotNull] IFetchedJob fetchedJob)
278 | {
279 | fetchedJob.RemoveFromQueue();
280 | }
281 | public override void Dispose()
282 | {
283 | //Don't have to dispose anything
284 | foreach (var lokc in _lockToDispose)
285 | {
286 | lokc.Dispose();
287 | }
288 | }
289 | }
290 | }
--------------------------------------------------------------------------------
/Hangfire.Redis.Tests/RedisWriteOnlyTransactionFacts.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using Hangfire.Common;
4 | using Hangfire.Redis.StackExchange;
5 | using Hangfire.Redis.Tests.Utils;
6 | using Hangfire.States;
7 | using Moq;
8 | using Xunit;
9 | using StackExchange.Redis;
10 |
11 | namespace Hangfire.Redis.Tests
12 | {
13 | [Collection("Sequential")]
14 | public class RedisWriteOnlyTransactionFacts
15 | {
16 | private readonly RedisStorage _storage;
17 | private readonly Mock _transaction;
18 |
19 | public RedisWriteOnlyTransactionFacts()
20 | {
21 | var options = new RedisStorageOptions() {Db = RedisUtils.GetDb()};
22 | _storage = new RedisStorage(RedisUtils.GetHostAndPort(), options);
23 |
24 | _transaction = new Mock();
25 | }
26 |
27 | [Fact]
28 | public void Ctor_ThrowsAnException_WhenStorageIsNull()
29 | {
30 | Assert.Throws("storage",
31 | () => new RedisWriteOnlyTransaction(null, _transaction.Object));
32 | }
33 |
34 | [Fact]
35 | public void Ctor_ThrowsAnException_WhenTransactionIsNull()
36 | {
37 | Assert.Throws("transaction",
38 | () => new RedisWriteOnlyTransaction(_storage, null));
39 | }
40 |
41 | [Fact, CleanRedis]
42 | public void ExpireJob_SetsExpirationDateForAllRelatedKeys()
43 | {
44 | UseConnection(redis =>
45 | {
46 | // Arrange
47 | redis.StringSet("{hangfire}:job:my-job", "job");
48 | redis.StringSet("{hangfire}:job:my-job:state", "state");
49 | redis.StringSet("{hangfire}:job:my-job:history", "history");
50 |
51 | // Act
52 | Commit(redis, x => x.ExpireJob("my-job", TimeSpan.FromDays(1)));
53 |
54 | // Assert
55 | var jobEntryTtl = redis.KeyTimeToLive("{hangfire}:job:my-job");
56 | var stateEntryTtl = redis.KeyTimeToLive("{hangfire}:job:my-job:state");
57 | var historyEntryTtl = redis.KeyTimeToLive("{hangfire}:job:my-job:state");
58 |
59 | Assert.True(TimeSpan.FromHours(23) < jobEntryTtl && jobEntryTtl < TimeSpan.FromHours(25));
60 | Assert.True(TimeSpan.FromHours(23) < stateEntryTtl && stateEntryTtl < TimeSpan.FromHours(25));
61 | Assert.True(TimeSpan.FromHours(23) < historyEntryTtl && historyEntryTtl < TimeSpan.FromHours(25));
62 | });
63 | }
64 |
65 | [Fact, CleanRedis]
66 | public void SetJobState_ModifiesJobEntry()
67 | {
68 | UseConnection(redis =>
69 | {
70 | // Arrange
71 | var state = new Mock();
72 | state.Setup(x => x.SerializeData()).Returns(new Dictionary());
73 | state.Setup(x => x.Name).Returns("my-state");
74 |
75 | // Act
76 | Commit(redis, x => x.SetJobState("my-job", state.Object));
77 |
78 | // Assert
79 | var hash = redis.HashGetAll("{hangfire}:job:my-job").ToStringDictionary();
80 | Assert.Equal("my-state", hash["State"]);
81 | });
82 | }
83 |
84 | [Fact, CleanRedis]
85 | public void SetJobState_RewritesStateEntry()
86 | {
87 | UseConnection(redis =>
88 | {
89 | // Arrange
90 | redis.HashSet("{hangfire}:job:my-job:state", "OldName", "OldValue");
91 |
92 | var state = new Mock();
93 | state.Setup(x => x.SerializeData()).Returns(
94 | new Dictionary
95 | {
96 | {"Name", "Value"}
97 | });
98 | state.Setup(x => x.Name).Returns("my-state");
99 | state.Setup(x => x.Reason).Returns("my-reason");
100 |
101 | // Act
102 | Commit(redis, x => x.SetJobState("my-job", state.Object));
103 |
104 | // Assert
105 | var stateHash = redis.HashGetAll("{hangfire}:job:my-job:state").ToStringDictionary();
106 | Assert.False(stateHash.ContainsKey("OldName"));
107 | Assert.Equal("my-state", stateHash["State"]);
108 | Assert.Equal("my-reason", stateHash["Reason"]);
109 | Assert.Equal("Value", stateHash["Name"]);
110 | });
111 | }
112 |
113 | [Fact, CleanRedis]
114 | public void SetJobState_AppendsJobHistoryList()
115 | {
116 | UseConnection(redis =>
117 | {
118 | // Arrange
119 | var state = new Mock();
120 | state.Setup(x => x.Name).Returns("my-state");
121 | state.Setup(x => x.SerializeData()).Returns(new Dictionary());
122 |
123 | // Act
124 | Commit(redis, x => x.SetJobState("my-job", state.Object));
125 |
126 | // Assert
127 | Assert.Equal(1, redis.ListLength("{hangfire}:job:my-job:history"));
128 | });
129 | }
130 |
131 | [Fact, CleanRedis]
132 | public void PersistJob_RemovesExpirationDatesForAllRelatedKeys()
133 | {
134 | UseConnection(redis =>
135 | {
136 | // Arrange
137 | redis.StringSet("{hangfire}:job:my-job", "job", TimeSpan.FromDays(1));
138 | redis.StringSet("{hangfire}:job:my-job:state", "state", TimeSpan.FromDays(1));
139 | redis.StringSet("{hangfire}:job:my-job:history", "history", TimeSpan.FromDays(1));
140 |
141 | // Act
142 | Commit(redis, x => x.PersistJob("my-job"));
143 |
144 | // Assert
145 | Assert.Null(redis.KeyTimeToLive("{hangfire}:job:my-job"));
146 | Assert.Null(redis.KeyTimeToLive("{hangfire}:job:my-job:state"));
147 | Assert.Null(redis.KeyTimeToLive("{hangfire}:job:my-job:history"));
148 | });
149 | }
150 |
151 | [Fact, CleanRedis]
152 | public void AddJobState_AddsJobHistoryEntry_AsJsonObject()
153 | {
154 | UseConnection(redis =>
155 | {
156 | // Arrange
157 | var state = new Mock();
158 | state.Setup(x => x.Name).Returns("my-state");
159 | state.Setup(x => x.Reason).Returns("my-reason");
160 | state.Setup(x => x.SerializeData()).Returns(
161 | new Dictionary {{"Name", "Value"}});
162 |
163 | // Act
164 | Commit(redis, x => x.AddJobState("my-job", state.Object));
165 |
166 | // Assert
167 | var serializedEntry = redis.ListGetByIndex("{hangfire}:job:my-job:history", 0);
168 | Assert.NotEqual(RedisValue.Null, serializedEntry);
169 |
170 |
171 | var entry = SerializationHelper.Deserialize>(serializedEntry);
172 | Assert.Equal("my-state", entry["State"]);
173 | Assert.Equal("my-reason", entry["Reason"]);
174 | Assert.Equal("Value", entry["Name"]);
175 | Assert.True(entry.ContainsKey("CreatedAt"));
176 | });
177 | }
178 |
179 | [Fact, CleanRedis]
180 | public void AddToQueue_AddsSpecifiedJobToTheQueue()
181 | {
182 | UseConnection(redis =>
183 | {
184 | Commit(redis, x => x.AddToQueue("critical", "my-job"));
185 |
186 | Assert.True(redis.SetContains("{hangfire}:queues", "critical"));
187 | Assert.Equal("my-job", (string) redis.ListGetByIndex("{hangfire}:queue:critical", 0));
188 | });
189 | }
190 |
191 | [Fact, CleanRedis]
192 | public void AddToQueue_PrependsListWithJob()
193 | {
194 | UseConnection(redis =>
195 | {
196 | redis.ListLeftPush("{hangfire}:queue:critical", "another-job");
197 |
198 | Commit(redis, x => x.AddToQueue("critical", "my-job"));
199 |
200 | Assert.Equal("my-job", (string) redis.ListGetByIndex("{hangfire}:queue:critical", 0));
201 | });
202 | }
203 |
204 | [Fact, CleanRedis]
205 | public void IncrementCounter_IncrementValueEntry()
206 | {
207 | UseConnection(redis =>
208 | {
209 | redis.StringSet("{hangfire}:entry", "3");
210 |
211 | Commit(redis, x => x.IncrementCounter("entry"));
212 |
213 | Assert.Equal("4", (string) redis.StringGet("{hangfire}:entry"));
214 | Assert.Null(redis.KeyTimeToLive("{hangfire}:entry"));
215 | });
216 | }
217 |
218 | [Fact, CleanRedis]
219 | public void IncrementCounter_WithExpiry_IncrementsValueAndSetsExpirationDate()
220 | {
221 | UseConnection(redis =>
222 | {
223 | redis.StringSet("{hangfire}:entry", "3");
224 |
225 | Commit(redis, x => x.IncrementCounter("entry", TimeSpan.FromDays(1)));
226 |
227 | var entryTtl = redis.KeyTimeToLive("{hangfire}:entry").Value;
228 | Assert.Equal("4", (string) redis.StringGet("{hangfire}:entry"));
229 | Assert.True(TimeSpan.FromHours(23) < entryTtl && entryTtl < TimeSpan.FromHours(25));
230 | });
231 | }
232 |
233 | [Fact, CleanRedis]
234 | public void DecrementCounter_DecrementsTheValueEntry()
235 | {
236 | UseConnection(redis =>
237 | {
238 | redis.StringSet("{hangfire}:entry", "3");
239 |
240 | Commit(redis, x => x.DecrementCounter("entry"));
241 |
242 | Assert.Equal("2", (string) redis.StringGet("{hangfire}:entry"));
243 | Assert.Null(redis.KeyTimeToLive("{hangfire}:entry"));
244 | });
245 | }
246 |
247 | [Fact, CleanRedis]
248 | public void DecrementCounter_WithExpiry_DecrementsTheValueAndSetsExpirationDate()
249 | {
250 | UseConnection(redis =>
251 | {
252 | redis.StringSet("{hangfire}:entry", "3");
253 | Commit(redis, x => x.DecrementCounter("entry", TimeSpan.FromDays(1)));
254 | var b = redis.KeyTimeToLive("{hangfire}:entry");
255 | var entryTtl = redis.KeyTimeToLive("{hangfire}:entry").Value;
256 | Assert.Equal("2", (string) redis.StringGet("{hangfire}:entry"));
257 | Assert.True(TimeSpan.FromHours(23) < entryTtl && entryTtl < TimeSpan.FromHours(25));
258 | });
259 | }
260 |
261 | [Fact, CleanRedis]
262 | public void AddToSet_AddsItemToSortedSet()
263 | {
264 | UseConnection(redis =>
265 | {
266 | Commit(redis, x => x.AddToSet("my-set", "my-value"));
267 |
268 | Assert.True(redis.SortedSetRank("{hangfire}:my-set", "my-value").HasValue);
269 | });
270 | }
271 |
272 | [Fact, CleanRedis]
273 | public void AddToSet_WithScore_AddsItemToSortedSetWithScore()
274 | {
275 | UseConnection(redis =>
276 | {
277 | Commit(redis, x => x.AddToSet("my-set", "my-value", 3.2));
278 |
279 | Assert.True(redis.SortedSetRank("{hangfire}:my-set", "my-value").HasValue);
280 | Assert.Equal(3.2, redis.SortedSetScore("{hangfire}:my-set", "my-value").Value, 3);
281 | });
282 | }
283 |
284 | [Fact, CleanRedis]
285 | public void RemoveFromSet_RemoveSpecifiedItemFromSortedSet()
286 | {
287 | UseConnection(redis =>
288 | {
289 | redis.SortedSetAdd("{hangfire}:my-set", "my-value", 0);
290 |
291 | Commit(redis, x => x.RemoveFromSet("my-set", "my-value"));
292 |
293 | Assert.False(redis.SortedSetRank("{hangfire}:my-set", "my-value").HasValue);
294 | });
295 | }
296 |
297 | [Fact, CleanRedis]
298 | public void InsertToList_PrependsListWithSpecifiedValue()
299 | {
300 | UseConnection(redis =>
301 | {
302 | redis.ListRightPush("{hangfire}:list", "value");
303 |
304 | Commit(redis, x => x.InsertToList("list", "new-value"));
305 | Assert.Equal("new-value", (string) redis.ListGetByIndex("{hangfire}:list", 0));
306 | });
307 | }
308 |
309 | [Fact, CleanRedis]
310 | public void RemoveFromList_RemovesAllGivenValuesFromList()
311 | {
312 | UseConnection(redis =>
313 | {
314 | redis.ListRightPush("{hangfire}:list", "value");
315 | redis.ListRightPush("{hangfire}:list", "another-value");
316 | redis.ListRightPush("{hangfire}:list", "value");
317 |
318 | Commit(redis, x => x.RemoveFromList("list", "value"));
319 |
320 | Assert.Equal(1, redis.ListLength("{hangfire}:list"));
321 | Assert.Equal("another-value", (string) redis.ListGetByIndex("{hangfire}:list", 0));
322 | });
323 | }
324 |
325 | [Fact, CleanRedis]
326 | public void TrimList_TrimsListToASpecifiedRange()
327 | {
328 | UseConnection(redis =>
329 | {
330 | redis.ListRightPush("{hangfire}:list", "1");
331 | redis.ListRightPush("{hangfire}:list", "2");
332 | redis.ListRightPush("{hangfire}:list", "3");
333 | redis.ListRightPush("{hangfire}:list", "4");
334 |
335 | Commit(redis, x => x.TrimList("list", 1, 2));
336 |
337 | Assert.Equal(2, redis.ListLength("{hangfire}:list"));
338 | Assert.Equal("2", (string) redis.ListGetByIndex("{hangfire}:list", 0));
339 | Assert.Equal("3", (string) redis.ListGetByIndex("{hangfire}:list", 1));
340 | });
341 | }
342 |
343 | [Fact, CleanRedis]
344 | public void SetRangeInHash_ThrowsAnException_WhenKeyIsNull()
345 | {
346 | UseConnection(redis =>
347 | {
348 | Assert.Throws("key",
349 | () => Commit(redis, x => x.SetRangeInHash(null, new Dictionary())));
350 | });
351 | }
352 |
353 | [Fact, CleanRedis]
354 | public void SetRangeInHash_ThrowsAnException_WhenKeyValuePairsArgumentIsNull()
355 | {
356 | UseConnection(redis =>
357 | {
358 | Assert.Throws("keyValuePairs",
359 | () => Commit(redis, x => x.SetRangeInHash("some-hash", null)));
360 | });
361 | }
362 |
363 | [Fact, CleanRedis]
364 | public void SetRangeInHash_SetsAllGivenKeyPairs()
365 | {
366 | UseConnection(redis =>
367 | {
368 | Commit(redis, x => x.SetRangeInHash("some-hash", new Dictionary
369 | {
370 | {"Key1", "Value1"},
371 | {"Key2", "Value2"}
372 | }));
373 |
374 | var hash = redis.HashGetAll("{hangfire}:some-hash").ToStringDictionary();
375 | Assert.Equal("Value1", hash["Key1"]);
376 | Assert.Equal("Value2", hash["Key2"]);
377 | });
378 | }
379 |
380 | [Fact, CleanRedis]
381 | public void RemoveHash_ThrowsAnException_WhenKeyIsNull()
382 | {
383 | UseConnection(redis =>
384 | {
385 | Assert.Throws(
386 | () => Commit(redis, x => x.RemoveHash(null)));
387 | });
388 | }
389 |
390 | [Fact, CleanRedis]
391 | public void RemoveHash_RemovesTheCorrespondingEntry()
392 | {
393 | UseConnection(redis =>
394 | {
395 | redis.HashSet("{hangfire}:some-hash", "key", "value");
396 |
397 | Commit(redis, x => x.RemoveHash("some-hash"));
398 |
399 | var hash = redis.HashGetAll("{hangfire}:some-hash");
400 | Assert.Empty(hash);
401 | });
402 | }
403 |
404 | private void Commit(IDatabase redis, Action action)
405 | {
406 | using (var transaction = new RedisWriteOnlyTransaction(_storage, redis.CreateTransaction()))
407 | {
408 | action(transaction);
409 | transaction.Commit();
410 | }
411 | }
412 |
413 | private static void UseConnection(Action action)
414 | {
415 | var redis = RedisUtils.CreateClient();
416 | action(redis);
417 | }
418 | }
419 | }
--------------------------------------------------------------------------------
/Hangfire.Redis.StackExchange/RedisConnection.cs:
--------------------------------------------------------------------------------
1 | // Copyright � 2013-2015 Sergey Odinokov, Marco Casamento
2 | // This software is based on https://github.com/HangfireIO/Hangfire.Redis
3 |
4 | // Hangfire.Redis.StackExchange is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as
6 | // published by the Free Software Foundation, either version 3
7 | // of the License, or any later version.
8 | //
9 | // Hangfire.Redis.StackExchange is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public
15 | // License along with Hangfire.Redis.StackExchange. If not, see .
16 |
17 | using Hangfire.Annotations;
18 | using Hangfire.Common;
19 | using Hangfire.Server;
20 | using Hangfire.Storage;
21 | using StackExchange.Redis;
22 | using System;
23 | using System.Collections.Concurrent;
24 | using System.Collections.Generic;
25 | using System.Globalization;
26 | using System.Linq;
27 | using System.Threading;
28 | using System.Threading.Tasks;
29 |
30 | namespace Hangfire.Redis.StackExchange
31 | {
32 | internal class RedisConnection : JobStorageConnection
33 | {
34 | private readonly RedisStorage _storage;
35 | private readonly RedisSubscription _subscription;
36 | private readonly TimeSpan _fetchTimeout = TimeSpan.FromMinutes(3);
37 | private readonly IServer redisServer;
38 | public RedisConnection(
39 | [NotNull] RedisStorage storage,
40 | [NotNull] IServer redisServer,
41 | [NotNull] IDatabase redis,
42 | [NotNull] RedisSubscription subscription,
43 | TimeSpan fetchTimeout)
44 | {
45 | _storage = storage ?? throw new ArgumentNullException(nameof(storage));
46 | this.redisServer = redisServer;
47 | _subscription = subscription ?? throw new ArgumentNullException(nameof(subscription));
48 | _fetchTimeout = fetchTimeout;
49 |
50 | Redis = redis ?? throw new ArgumentNullException(nameof(redis));
51 | }
52 |
53 | public IDatabase Redis { get; }
54 |
55 | public override IDisposable AcquireDistributedLock([NotNull] string resource, TimeSpan timeout)
56 | {
57 | return RedisLock.Acquire(Redis, _storage.GetRedisKey(resource), timeout);
58 | }
59 |
60 | public override void AnnounceServer([NotNull] string serverId, [NotNull] ServerContext context)
61 | {
62 | if (serverId == null) throw new ArgumentNullException(nameof(serverId));
63 | if (context == null) throw new ArgumentNullException(nameof(context));
64 |
65 | if (_storage.UseTransactions)
66 | {
67 | var transaction = Redis.CreateTransaction();
68 |
69 | _ = transaction.SetAddAsync(_storage.GetRedisKey("servers"), serverId);
70 |
71 | _ = transaction.HashSetAsync(
72 | _storage.GetRedisKey($"server:{serverId}"),
73 | new Dictionary
74 | {
75 | { "WorkerCount", context.WorkerCount.ToString(CultureInfo.InvariantCulture) },
76 | { "StartedAt", JobHelper.SerializeDateTime(DateTime.UtcNow) },
77 | }.ToHashEntries());
78 |
79 | if (context.Queues.Length > 0)
80 | {
81 | _ = transaction.ListRightPushAsync(
82 | _storage.GetRedisKey($"server:{serverId}:queues"),
83 | context.Queues.ToRedisValues());
84 | }
85 |
86 | _ = transaction.Execute();
87 | }
88 | else
89 | {
90 | var tasks = new Task[3];
91 | tasks[0] = Redis.SetAddAsync(_storage.GetRedisKey("servers"), serverId);
92 |
93 | tasks[1] = Redis.HashSetAsync(
94 | _storage.GetRedisKey($"server:{serverId}"),
95 | new Dictionary
96 | {
97 | { "WorkerCount", context.WorkerCount.ToString(CultureInfo.InvariantCulture) },
98 | { "StartedAt", JobHelper.SerializeDateTime(DateTime.UtcNow) },
99 | }.ToHashEntries());
100 |
101 | if (context.Queues.Length > 0)
102 | {
103 | tasks[2] = Redis.ListRightPushAsync(
104 | _storage.GetRedisKey($"server:{serverId}:queues"),
105 | context.Queues.ToRedisValues());
106 | }
107 | else
108 | {
109 | tasks[2] = Task.CompletedTask;;
110 | }
111 |
112 | Task.WaitAll(tasks);
113 | }
114 | }
115 |
116 | public override DateTime GetUtcDateTime()
117 | {
118 | //the returned time is the time on the first server of the cluster
119 | return redisServer.Time();
120 | }
121 |
122 | public override long GetSetCount([NotNull] IEnumerable keys, int limit)
123 | {
124 | Task[] tasks = new Task[keys.Count()];
125 | int i = 0;
126 | IBatch batch = Redis.CreateBatch();
127 | ConcurrentDictionary results = new ConcurrentDictionary();
128 | foreach (string key in keys)
129 | {
130 | tasks[i] = batch.SortedSetLengthAsync(_storage.GetRedisKey(key), max: limit)
131 | .ContinueWith((Task x) => results.TryAdd(_storage.GetRedisKey(key), x.Result));
132 | }
133 | batch.Execute();
134 | Task.WaitAll(tasks);
135 | return results.Sum(x => x.Value);
136 | }
137 |
138 | public override bool GetSetContains([NotNull] string key, [NotNull] string value)
139 | {
140 | var sortedSetEntries = Redis.SortedSetScan(_storage.GetRedisKey(key), value);
141 | return sortedSetEntries.Any();
142 | }
143 |
144 | public override string CreateExpiredJob(
145 | [NotNull] Job job,
146 | [NotNull] IDictionary parameters,
147 | DateTime createdAt,
148 | TimeSpan expireIn)
149 | {
150 | if (job == null) throw new ArgumentNullException(nameof(job));
151 | if (parameters == null) throw new ArgumentNullException(nameof(parameters));
152 |
153 | var jobId = Guid.NewGuid().ToString();
154 |
155 | var invocationData = InvocationData.SerializeJob(job);
156 |
157 | // Do not modify the original parameters.
158 | var storedParameters = new Dictionary(parameters)
159 | {
160 | { "Type", invocationData.Type },
161 | { "Method", invocationData.Method },
162 | { "ParameterTypes", invocationData.ParameterTypes },
163 | { "Arguments", invocationData.Arguments },
164 | { "CreatedAt", JobHelper.SerializeDateTime(createdAt) }
165 | };
166 |
167 | if (invocationData.Queue != null)
168 | {
169 | storedParameters.Add("Queue", invocationData.Queue);
170 | }
171 |
172 | if (_storage.UseTransactions)
173 | {
174 | var transaction = Redis.CreateTransaction();
175 |
176 | _ = transaction.HashSetAsync(_storage.GetRedisKey($"job:{jobId}"), storedParameters.ToHashEntries());
177 | _ = transaction.KeyExpireAsync(_storage.GetRedisKey($"job:{jobId}"), expireIn);
178 |
179 | if (!transaction.Execute())
180 | throw new HangfireRedisTransactionException("Transaction Execution failure");
181 | }
182 | else
183 | {
184 | var tasks = new Task[2];
185 | tasks[0] = Redis.HashSetAsync(_storage.GetRedisKey($"job:{jobId}"), storedParameters.ToHashEntries());
186 | tasks[1] = Redis.KeyExpireAsync(_storage.GetRedisKey($"job:{jobId}"), expireIn);
187 | Task.WaitAll(tasks);
188 | }
189 |
190 | return jobId;
191 | }
192 |
193 | public override IWriteOnlyTransaction CreateWriteTransaction()
194 | {
195 | if (_storage.UseTransactions)
196 | {
197 | return new RedisWriteOnlyTransaction(_storage, Redis.CreateTransaction());
198 | }
199 | return new RedisWriteDirectlyToDatabase(_storage, Redis);
200 | }
201 |
202 | public override void Dispose()
203 | {
204 | // nothing to dispose
205 | }
206 |
207 | public override IFetchedJob FetchNextJob([NotNull] string[] queues, CancellationToken cancellationToken)
208 | {
209 | if (queues == null) throw new ArgumentNullException(nameof(queues));
210 |
211 | string jobId = null;
212 | string queueName = null;
213 | do
214 | {
215 | cancellationToken.ThrowIfCancellationRequested();
216 |
217 | for (int i = 0; i < queues.Length; i++)
218 | {
219 | queueName = queues[i];
220 | var queueKey = _storage.GetRedisKey($"queue:{queueName}");
221 | var fetchedKey = _storage.GetRedisKey($"queue:{queueName}:dequeued");
222 | jobId = Redis.ListRightPopLeftPush(queueKey, fetchedKey);
223 | if (jobId != null) break;
224 | }
225 |
226 | if (jobId == null)
227 | {
228 | _subscription.WaitForJob(_fetchTimeout, cancellationToken);
229 | }
230 | }
231 | while (jobId == null);
232 |
233 | // The job was fetched by the server. To provide reliability,
234 | // we should ensure, that the job will be performed and acquired
235 | // resources will be disposed even if the server will crash
236 | // while executing one of the subsequent lines of code.
237 |
238 | // The job's processing is splitted into a couple of checkpoints.
239 | // Each checkpoint occurs after successful update of the
240 | // job information in the storage. And each checkpoint describes
241 | // the way to perform the job when the server was crashed after
242 | // reaching it.
243 |
244 | // Checkpoint #1-1. The job was fetched into the fetched list,
245 | // that is being inspected by the FetchedJobsWatcher instance.
246 | // Job's has the implicit 'Fetched' state.
247 |
248 | //strip out microseconds from fetchTime as the serialized version will anyway truncate them
249 | var fetchTime = new DateTime(DateTime.UtcNow.Ticks / 10000 * 10000);
250 |
251 | _ = Redis.HashSet(
252 | _storage.GetRedisKey($"job:{jobId}"),
253 | "Fetched",
254 | JobHelper.SerializeDateTime(fetchTime));
255 |
256 | // Checkpoint #2. The job is in the implicit 'Fetched' state now.
257 | // This state stores information about fetched time. The job will
258 | // be re-queued when the JobTimeout will be expired.
259 |
260 | return new RedisFetchedJob(_storage, Redis, jobId, queueName, fetchTime);
261 | }
262 |
263 | public override Dictionary GetAllEntriesFromHash([NotNull] string key)
264 | {
265 | var result = Redis.HashGetAll(_storage.GetRedisKey(key)).ToStringDictionary();
266 |
267 | return result.Count != 0 ? result : null;
268 | }
269 |
270 | public override List GetAllItemsFromList([NotNull] string key)
271 | {
272 | return Redis.ListRange(_storage.GetRedisKey(key)).ToStringArray().ToList();
273 | }
274 |
275 | public override HashSet GetAllItemsFromSet([NotNull] string key)
276 | {
277 | HashSet result = new HashSet();
278 | foreach (var item in Redis.SortedSetScan(_storage.GetRedisKey(key)))
279 | {
280 | _ = result.Add(item.Element);
281 | }
282 |
283 | return result;
284 | }
285 |
286 | public override long GetCounter([NotNull] string key)
287 | {
288 | return Convert.ToInt64(Redis.StringGet(_storage.GetRedisKey(key)));
289 | }
290 |
291 | public override string GetFirstByLowestScoreFromSet([NotNull] string key, double fromScore, double toScore)
292 | {
293 | return Redis.SortedSetRangeByScore(_storage.GetRedisKey(key), fromScore, toScore, skip: 0, take: 1)
294 | .FirstOrDefault();
295 | }
296 |
297 | public override List GetFirstByLowestScoreFromSet(string key, double fromScore, double toScore, int count)
298 | {
299 | return Redis.SortedSetRangeByScore(_storage.GetRedisKey(key), fromScore, toScore, skip: 0, take: count)
300 | .Where(x => x.HasValue)
301 | .Select(x => x.ToString())
302 | .ToList();
303 | }
304 |
305 | public override long GetHashCount([NotNull] string key)
306 | {
307 | return Redis.HashLength(_storage.GetRedisKey(key));
308 | }
309 |
310 | public override TimeSpan GetHashTtl([NotNull] string key)
311 | {
312 | return Redis.KeyTimeToLive(_storage.GetRedisKey(key)) ?? TimeSpan.Zero;
313 | }
314 |
315 | public override JobData GetJobData([NotNull] string jobId)
316 | {
317 | if (jobId == null) throw new ArgumentNullException(nameof(jobId));
318 |
319 | var storedData = Redis.HashGetAll(_storage.GetRedisKey($"job:{jobId}"));
320 | if (storedData.Length == 0) return null;
321 |
322 | string queue = storedData.FirstOrDefault(x => x.Name == "Queue").Value;
323 | string type = storedData.FirstOrDefault(x => x.Name == "Type").Value;
324 | string method = storedData.FirstOrDefault(x => x.Name == "Method").Value;
325 | string parameterTypes = storedData.FirstOrDefault(x => x.Name == "ParameterTypes").Value;
326 | string arguments = storedData.FirstOrDefault(x => x.Name == "Arguments").Value;
327 | string createdAt = storedData.FirstOrDefault(x => x.Name == "CreatedAt").Value;
328 |
329 | Job job = null;
330 | JobLoadException loadException = null;
331 |
332 | var invocationData = new InvocationData(type, method, parameterTypes, arguments, queue);
333 |
334 | try
335 | {
336 | job = invocationData.DeserializeJob();
337 | }
338 | catch (JobLoadException ex)
339 | {
340 | loadException = ex;
341 | }
342 |
343 | return new JobData
344 | {
345 | Job = job,
346 | State = storedData.FirstOrDefault(x => x.Name == "State").Value,
347 | CreatedAt = JobHelper.DeserializeNullableDateTime(createdAt) ?? DateTime.MinValue,
348 | LoadException = loadException
349 | };
350 | }
351 |
352 | public override string GetJobParameter([NotNull] string jobId, [NotNull] string name)
353 | {
354 | if (jobId == null) throw new ArgumentNullException(nameof(jobId));
355 | if (name == null) throw new ArgumentNullException(nameof(name));
356 |
357 | return Redis.HashGet(_storage.GetRedisKey($"job:{jobId}"), name);
358 | }
359 |
360 | public override long GetListCount([NotNull] string key)
361 | {
362 | return Redis.ListLength(_storage.GetRedisKey(key));
363 | }
364 |
365 | public override TimeSpan GetListTtl([NotNull] string key)
366 | {
367 | return Redis.KeyTimeToLive(_storage.GetRedisKey(key)) ?? TimeSpan.Zero;
368 | }
369 |
370 | public override List GetRangeFromList([NotNull] string key, int startingFrom, int endingAt)
371 | {
372 | return Redis.ListRange(_storage.GetRedisKey(key), startingFrom, endingAt).ToStringArray().ToList();
373 | }
374 |
375 | public override List GetRangeFromSet([NotNull] string key, int startingFrom, int endingAt)
376 | {
377 | return Redis.SortedSetRangeByRank(_storage.GetRedisKey(key), startingFrom, endingAt).ToStringArray().ToList();
378 | }
379 |
380 | public override long GetSetCount([NotNull] string key)
381 | {
382 | return Redis.SortedSetLength(_storage.GetRedisKey(key));
383 | }
384 |
385 | public override TimeSpan GetSetTtl([NotNull] string key)
386 | {
387 | return Redis.KeyTimeToLive(_storage.GetRedisKey(key)) ?? TimeSpan.Zero;
388 | }
389 |
390 | public override StateData GetStateData([NotNull] string jobId)
391 | {
392 | if (jobId == null) throw new ArgumentNullException(nameof(jobId));
393 |
394 | var entries = Redis.HashGetAll(_storage.GetRedisKey($"job:{jobId}:state"));
395 | if (entries.Length == 0) return null;
396 |
397 | var stateData = entries.ToStringDictionary();
398 |
399 | _ = stateData.Remove("State");
400 | _ = stateData.Remove("Reason");
401 |
402 | return new StateData
403 | {
404 | Name = entries.First(x => x.Name == "State").Value,
405 | Reason = entries.FirstOrDefault(x => x.Name == "Reason").Value,
406 | Data = stateData
407 | };
408 | }
409 |
410 | public override string GetValueFromHash([NotNull] string key, [NotNull] string name)
411 | {
412 | if (name == null) throw new ArgumentNullException(nameof(name));
413 |
414 | return Redis.HashGet(_storage.GetRedisKey(key), name);
415 | }
416 |
417 | public override void Heartbeat([NotNull] string serverId)
418 | {
419 | if (serverId == null) throw new ArgumentNullException(nameof(serverId));
420 |
421 | _ = Redis.HashSet(
422 | _storage.GetRedisKey($"server:{serverId}"),
423 | "Heartbeat",
424 | JobHelper.SerializeDateTime(DateTime.UtcNow));
425 | }
426 |
427 | public override void RemoveServer([NotNull] string serverId)
428 | {
429 | if (serverId == null) throw new ArgumentNullException(nameof(serverId));
430 |
431 | if (_storage.UseTransactions)
432 | {
433 | var transaction = Redis.CreateTransaction();
434 |
435 | _ = transaction.SetRemoveAsync(_storage.GetRedisKey("servers"), serverId);
436 |
437 | _ = transaction.KeyDeleteAsync(
438 | new RedisKey[]
439 | {
440 | _storage.GetRedisKey($"server:{serverId}"),
441 | _storage.GetRedisKey($"server:{serverId}:queues")
442 | });
443 |
444 | _ = transaction.Execute();
445 | }
446 | else
447 | {
448 | var tasks = new Task[2];
449 | tasks[0] = Redis.SetRemoveAsync(_storage.GetRedisKey("servers"), serverId);
450 | tasks[1] = Redis.KeyDeleteAsync(
451 | new RedisKey[]
452 | {
453 | _storage.GetRedisKey($"server:{serverId}"),
454 | _storage.GetRedisKey($"server:{serverId}:queues")
455 | });
456 | Task.WaitAll(tasks);
457 | }
458 | }
459 |
460 | public override int RemoveTimedOutServers(TimeSpan timeOut)
461 | {
462 | var serverNames = Redis.SetMembers(_storage.GetRedisKey("servers"));
463 | var heartbeats = new Dictionary>();
464 |
465 | var utcNow = DateTime.UtcNow;
466 |
467 | foreach (var serverName in serverNames)
468 | {
469 | var srv = Redis.HashGet(_storage.GetRedisKey($"server:{serverName}"), new RedisValue[] { "StartedAt", "Heartbeat" });
470 | heartbeats.Add(serverName,
471 | new Tuple(
472 | JobHelper.DeserializeDateTime(srv[0]),
473 | JobHelper.DeserializeNullableDateTime(srv[1])));
474 | }
475 |
476 | var removedServerCount = 0;
477 | foreach (var heartbeat in heartbeats)
478 | {
479 | var maxTime = new DateTime(
480 | Math.Max(heartbeat.Value.Item1.Ticks, (heartbeat.Value.Item2 ?? DateTime.MinValue).Ticks));
481 |
482 | if (utcNow > maxTime.Add(timeOut))
483 | {
484 | RemoveServer(heartbeat.Key);
485 | removedServerCount++;
486 | }
487 | }
488 |
489 | return removedServerCount;
490 | }
491 |
492 | public override void SetJobParameter([NotNull] string jobId, [NotNull] string name, string value)
493 | {
494 | if (jobId == null) throw new ArgumentNullException(nameof(jobId));
495 | if (name == null) throw new ArgumentNullException(nameof(name));
496 |
497 | _ = Redis.HashSet(_storage.GetRedisKey($"job:{jobId}"), name, value);
498 | }
499 |
500 | public override void SetRangeInHash([NotNull] string key, [NotNull] IEnumerable> keyValuePairs)
501 | {
502 | if (keyValuePairs == null) throw new ArgumentNullException(nameof(keyValuePairs));
503 |
504 | Redis.HashSet(_storage.GetRedisKey(key), keyValuePairs.ToHashEntries());
505 | }
506 | }
507 | }
508 |
--------------------------------------------------------------------------------