diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..ded96b2
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,5 @@
+root = true
+
+[*.cs,*.ps1]
+indent_style = space
+indent_size = 4
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..3853718
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,22 @@
+# Auto detect text files and perform LF normalization
+* text=unset
+
+# Custom for Visual Studio
+*.cs diff=csharp
+*.sln merge=union
+*.csproj merge=union
+*.vbproj merge=union
+*.fsproj merge=union
+*.dbproj merge=union
+
+# Standard to msysgit
+*.doc diff=astextplain
+*.DOC diff=astextplain
+*.docx diff=astextplain
+*.DOCX diff=astextplain
+*.dot diff=astextplain
+*.DOT diff=astextplain
+*.pdf diff=astextplain
+*.PDF diff=astextplain
+*.rtf diff=astextplain
+*.RTF diff=astextplain
diff --git a/.nuget/NuGet.exe b/.nuget/NuGet.exe
new file mode 100644
index 0000000..9cba6ed
Binary files /dev/null and b/.nuget/NuGet.exe differ
diff --git a/.nuget/packages.config b/.nuget/packages.config
new file mode 100644
index 0000000..26fa2dc
--- /dev/null
+++ b/.nuget/packages.config
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Hangfire.Mono.sln b/Hangfire.Mono.sln
new file mode 100644
index 0000000..313581d
--- /dev/null
+++ b/Hangfire.Mono.sln
@@ -0,0 +1,57 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 2012
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hangfire.Core", "src\Hangfire.Core\Hangfire.Core.csproj", "{C995EA9E-56EE-4951-8260-D94260A7F4C2}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hangfire.SqlServer", "src\Hangfire.SqlServer\Hangfire.SqlServer.csproj", "{A523C0E3-097D-4869-977F-15A717EA3E83}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hangfire.SqlServer.Msmq", "src\Hangfire.SqlServer.Msmq\Hangfire.SqlServer.Msmq.csproj", "{762BE479-0AEC-47E0-8F9C-34FA54641749}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{9FC4F973-0AA1-4E6E-AF9A-561629BE50EC}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hangfire.Core.Tests", "tests\Hangfire.Core.Tests\Hangfire.Core.Tests.csproj", "{E13C3543-39A3-475C-BB43-2E311E634843}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hangfire.SqlServer.RabbitMq", "src\Hangfire.SqlServer.RabbitMq\Hangfire.SqlServer.RabbitMq.csproj", "{4CC51F69-0311-4485-B7DE-9ECAB3A1B5E5}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hangfire.SqlServer.RabbitMq.Tests", "tests\Hangfire.SqlServer.RabbitMq.Tests\Hangfire.SqlServer.RabbitMq.Tests.csproj", "{2654D751-33D5-411D-A187-D057FED9CD25}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {C995EA9E-56EE-4951-8260-D94260A7F4C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C995EA9E-56EE-4951-8260-D94260A7F4C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C995EA9E-56EE-4951-8260-D94260A7F4C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C995EA9E-56EE-4951-8260-D94260A7F4C2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A523C0E3-097D-4869-977F-15A717EA3E83}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A523C0E3-097D-4869-977F-15A717EA3E83}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A523C0E3-097D-4869-977F-15A717EA3E83}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A523C0E3-097D-4869-977F-15A717EA3E83}.Release|Any CPU.Build.0 = Release|Any CPU
+ {762BE479-0AEC-47E0-8F9C-34FA54641749}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {762BE479-0AEC-47E0-8F9C-34FA54641749}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {762BE479-0AEC-47E0-8F9C-34FA54641749}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {762BE479-0AEC-47E0-8F9C-34FA54641749}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E13C3543-39A3-475C-BB43-2E311E634843}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E13C3543-39A3-475C-BB43-2E311E634843}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E13C3543-39A3-475C-BB43-2E311E634843}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E13C3543-39A3-475C-BB43-2E311E634843}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4CC51F69-0311-4485-B7DE-9ECAB3A1B5E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4CC51F69-0311-4485-B7DE-9ECAB3A1B5E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4CC51F69-0311-4485-B7DE-9ECAB3A1B5E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4CC51F69-0311-4485-B7DE-9ECAB3A1B5E5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2654D751-33D5-411D-A187-D057FED9CD25}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2654D751-33D5-411D-A187-D057FED9CD25}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2654D751-33D5-411D-A187-D057FED9CD25}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2654D751-33D5-411D-A187-D057FED9CD25}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {E13C3543-39A3-475C-BB43-2E311E634843} = {9FC4F973-0AA1-4E6E-AF9A-561629BE50EC}
+ {25909EE6-2A56-4EAC-9D0E-436FF2435E88} = {9FC4F973-0AA1-4E6E-AF9A-561629BE50EC}
+ {2654D751-33D5-411D-A187-D057FED9CD25} = {9FC4F973-0AA1-4E6E-AF9A-561629BE50EC}
+ EndGlobalSection
+ GlobalSection(MonoDevelopProperties) = preSolution
+ StartupItem = src\Hangfire.Core\Hangfire.Core.csproj
+ EndGlobalSection
+EndGlobal
diff --git a/Hangfire.Raven.sln b/Hangfire.Raven.sln
new file mode 100644
index 0000000..13f30d9
--- /dev/null
+++ b/Hangfire.Raven.sln
@@ -0,0 +1,45 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 14
+VisualStudioVersion = 14.0.23107.0
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleSample", "samples\ConsoleSample\ConsoleSample.csproj", "{C02BB718-2AE4-434C-8668-C894FF663FCE}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{119DA7FA-B94C-4B63-AEC9-428EF834E0D8}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hangfire.Raven", "src\Hangfire.SqlServer\Hangfire.Raven.csproj", "{A523C0E3-097D-4869-977F-15A717EA3E83}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{766BE831-F758-46BC-AFD3-BBEEFE0F686F}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "nuspecs", "nuspecs", "{5E687025-A525-4534-8869-CB685C7B11C4}"
+ ProjectSection(SolutionItems) = preProject
+ nuspecs\Hangfire.Raven.nuspec = nuspecs\Hangfire.Raven.nuspec
+ EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{F710C9CF-69F9-4373-9B74-ABB185BF985B}"
+ ProjectSection(SolutionItems) = preProject
+ .nuget\packages.config = .nuget\packages.config
+ EndProjectSection
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {C02BB718-2AE4-434C-8668-C894FF663FCE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C02BB718-2AE4-434C-8668-C894FF663FCE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C02BB718-2AE4-434C-8668-C894FF663FCE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C02BB718-2AE4-434C-8668-C894FF663FCE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A523C0E3-097D-4869-977F-15A717EA3E83}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A523C0E3-097D-4869-977F-15A717EA3E83}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A523C0E3-097D-4869-977F-15A717EA3E83}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A523C0E3-097D-4869-977F-15A717EA3E83}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {C02BB718-2AE4-434C-8668-C894FF663FCE} = {119DA7FA-B94C-4B63-AEC9-428EF834E0D8}
+ EndGlobalSection
+EndGlobal
diff --git a/Hangfire.sln.DotSettings b/Hangfire.sln.DotSettings
new file mode 100644
index 0000000..1b8e8e4
--- /dev/null
+++ b/Hangfire.sln.DotSettings
@@ -0,0 +1,6 @@
+
+ True
+ True
+ True
+ <data><IncludeFilters /><ExcludeFilters><Filter ModuleMask="Hangfire.Core.Tests" ModuleVersionMask="*" ClassMask="*" FunctionMask="*" IsEnabled="True" /><Filter ModuleMask="Hangfire.SqlServer.Tests" ModuleVersionMask="*" ClassMask="*" FunctionMask="*" IsEnabled="True" /></ExcludeFilters></data>
+ <data />
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
deleted file mode 100644
index 8f71f43..0000000
--- a/LICENSE
+++ /dev/null
@@ -1,202 +0,0 @@
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
-
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
- 1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
- APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "{}"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright {yyyy} {name of copyright owner}
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..9bc3133
--- /dev/null
+++ b/README.md
@@ -0,0 +1,170 @@
+This is a work on progress! Don't use it yet.
+
+
+Hangfire
+=========
+
+[](http://hangfire.io) [](https://www.nuget.org/packages?q=hangfire) [](https://ci.appveyor.com/project/odinserj/hangfire-525) [](https://travis-ci.org/HangfireIO/Hangfire) [](http://www.gnu.org/licenses/lgpl-3.0.html)
+
+Incredibly easy way to perform **fire-and-forget**, **delayed** and **recurring jobs** inside **ASP.NET applications**. CPU and I/O intensive, long-running and short-running jobs are supported. No Windows Service / Task Scheduler required. Backed by Redis, SQL Server, SQL Azure or MSMQ.
+
+Hangfire provides unified programming model to handle background tasks in a **reliable way** and run them on shared hosting, dedicated hosting or in cloud. You can start with a simple setup and grow computational power for background jobs with time for these scenarios:
+
+- mass notifications/newsletter;
+- batch import from xml, csv, json;
+- creation of archives;
+- firing off web hooks;
+- deleting users;
+- building different graphs;
+- image/video processing;
+- purge temporary files;
+- recurring automated reports;
+- database maintenance;
+- *…and so on.*
+
+Hangfire is a .NET Framework alternative to [Resque](https://github.com/resque/resque), [Sidekiq](http://sidekiq.org), [delayed_job](https://github.com/collectiveidea/delayed_job), [Celery](http://www.celeryproject.org).
+
+
+
+Installation
+-------------
+
+Hangfire is available as a NuGet package. So, you can install it using the NuGet Package Console window:
+
+```
+PM> Install-Package Hangfire
+```
+
+After install, update your existing [OWIN Startup](http://www.asp.net/aspnet/overview/owin-and-katana/owin-startup-class-detection) file with the following lines of code. If you do not have this class in your project or don't know what is it, please read the [Quick start](http://docs.hangfire.io/en/latest/quickstart.html) guide to learn about how to install Hangfire.
+
+```csharp
+public void Configuration(IAppBuilder app)
+{
+ GlobalConfiguration.Configuration.UseSqlServerStorage("");
+
+ app.UseHangfireServer();
+ app.UseHangfireDashboard();
+}
+```
+
+Usage
+------
+
+This is incomplete list of features, to see all of them, check the [official site](http://hangfire.io) and the [documentation](http://docs.hangfire.io).
+
+[**Fire-and-forget tasks**](http://docs.hangfire.io/en/latest/users-guide/background-methods/calling-methods-in-background.html)
+
+Enqueued background jobs are being executed inside a dedicated worker pool threads as soon as possible, shortening your request processing time.
+
+```csharp
+BackgroundJob.Enqueue(() => Console.WriteLine("Simple!"));
+```
+
+[**Delayed tasks**](http://docs.hangfire.io/en/latest/users-guide/background-methods/calling-methods-with-delay.html)
+
+Scheduled background jobs are being executed only after given amount of time.
+
+```csharp
+BackgroundJob.Schedule(() => Console.WriteLine("Reliable!"), TimeSpan.FromDays(7));
+```
+
+[**Recurring tasks**](http://docs.hangfire.io/en/latest/users-guide/background-methods/performing-recurrent-tasks.html)
+
+Recurring jobs were never been simpler, just call the following method to perform any kind of recurring task using the [CRON expressions](http://en.wikipedia.org/wiki/Cron#CRON_expression).
+
+```csharp
+RecurringJob.AddOrUpdate(() => Console.WriteLine("Transparent!"), Cron.Daily);
+```
+
+**Continuations**
+
+Continuations allow you to define complex workflows by chaining multiple background jobs together.
+
+```csharp
+var id = BackgroundJob.Enqueue(() => Console.WriteLine("Hello, "));
+BackgroundJob.ContinueWith(id, () => Console.WriteLine("world!"));
+```
+
+**Process them inside a web application…**
+
+You can process background tasks in any OWIN compatible application frameworks, including [ASP.NET MVC](http://www.asp.net/mvc), [ASP.NET Web API](http://www.asp.net/web-api), [FubuMvc](http://fubu-project.org), [Nancy](http://nancyfx.org), etc. Forget about [AppDomain unloads, Web Garden & Web Farm issues](http://haacked.com/archive/2011/10/16/the-dangers-of-implementing-recurring-background-tasks-in-asp-net.aspx/) – Hangfire is reliable for web applications from scratch, even on shared hosting.
+
+```csharp
+app.UseHangfireServer();
+```
+
+**… or anywhere else**
+
+In console application, Windows Service, Azure Worker Role, etc.
+
+```csharp
+using (new BackgroundJobServer())
+{
+ Console.WriteLine("Hangfire Server started. Press ENTER to exit...");
+ Console.ReadLine();
+}
+```
+
+Questions? Problems?
+---------------------
+
+Open-source project are developing more smoothly, when all discussions are held in public.
+
+If you have any questions, problems related to the Hangfire usage or want to discuss new features, please visit the [discussion forum](http://discuss.hangfire.io). You can sign in there using your existing Google or GitHub account, so it's very simple to start using it.
+
+If you've discovered a bug, please report it to the [Hangfire GitHub Issues](https://github.com/odinserj/Hangfire/issues?state=open). Detailed reports with stack traces, actual and expected behavours are welcome.
+
+Related Projects
+-----------------
+
+Please see the [Extensions](http://hangfire.io/extensions.html) page on official site.
+
+Building the sources
+---------------------
+
+To build a solution and get assembly files, just run the following command. All build artifacts, including `*.pdb` files will be placed into the `build` folder. **Before proposing a pull request, please use this command to ensure everything is ok.** Btw, you can execute this command from Package Manager Console window.
+
+```
+build
+```
+
+To build NuGet packages as well as an archive file, use the `pack` command as shown below. You can find the result files in the `build` folder.
+
+```
+build pack
+```
+
+To see the full list of avalable commands, pass the `-docs` switch:
+
+```
+build -docs
+```
+
+Hangfire uses [psake](https://github.com/psake/psake) build automation tool. All psake tasks and functions defined in `psake-build.ps1` (for this project) and `psake-common.ps1` (for other Hangfire projects) files. Thanks to the psake project, they are very simple to use and modify!
+
+License
+--------
+
+Copyright © 2013-2014 Sergey Odinokov.
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with this program. If not, see [http://www.gnu.org/licenses/](http://www.gnu.org/licenses).
+
+Legal
+------
+
+By submitting a Pull Request, you disavow any rights or claims to any changes submitted to the Hangfire project and assign the copyright of those changes to Sergey Odinokov.
+
+If you cannot or do not want to reassign those rights (your employment contract for your employer may not allow this), you should not submit a PR. Open an issue and someone else can do the work.
+
+This is a legal way of saying "If you submit a PR to us, that code becomes ours". 99.9% of the time that's what you intend anyways; we hope it doesn't scare you away from contributing.
diff --git a/content/readme.txt b/content/readme.txt
new file mode 100644
index 0000000..dfb958f
--- /dev/null
+++ b/content/readme.txt
@@ -0,0 +1,37 @@
+Please see http://docs.hangfire.io for more information on using Hangfire. The
+`Hangfire` package is using SQL Server as a job storage and intended to run in
+any OWIN-based web application.
+
+To run Hangfire in your application, update the existing OWIN Startup class or
+create the file `Startup.cs` in the root folder of your application as shown
+below. Please see also http://docs.hangfire.io/en/latest/quickstart.html.
+
+!!! DASHBOARD REQUIRES AUTH CONFIGURATION !!!
+
+By default, ONLY LOCAL requests are allowed to access the Dashboard. Please
+see the `Configuring Dashboard authorization` section in Hangfire documentation:
+http://docs.hangfire.io/en/latest/configuration/configuring-authorization.html
+
+Sample OWIN Startup class
+--------------------------
+
+using Hangfire;
+using Microsoft.Owin;
+using Owin;
+
+[assembly: OwinStartup(typeof(MyWebApplication.Startup))]
+
+namespace MyWebApplication
+{
+ public class Startup
+ {
+ public void Configuration(IAppBuilder app)
+ {
+ GlobalConfiguration.Configuration
+ .UseSqlServerStorage("");
+
+ app.UseHangfireDashboard();
+ app.UseHangfireServer();
+ }
+ }
+}
diff --git a/nuspecs/Hangfire.Core.nuspec b/nuspecs/Hangfire.Core.nuspec
new file mode 100644
index 0000000..42a6cee
--- /dev/null
+++ b/nuspecs/Hangfire.Core.nuspec
@@ -0,0 +1,54 @@
+
+
+
+ Hangfire.Core
+ 0.0.0
+ Hangfire Core Components
+ Sergey Odinokov
+ odinserj
+ http://hangfire.io/
+ false
+ https://raw.github.com/HangfireIO/Hangfire/master/LICENSE.md
+ Core components for Hangfire (background job system for ASP.NET applications).
+ Copyright © 2013-2015 Sergey Odinokov
+ Hangfire OWIN Long-Running Background Fire-And-Forget Delayed Recurring Tasks Jobs Scheduler Threading Queues
+ http://hangfire.io/blog/
+
+1.5.0
+* Added – `IBackgroundProcess` interface that allows to implement custom background processes.
+* Added – `BackgroundProcessingServer` class for creating a server instance with completely custom background processes.
+* Added – Support for running multiple `BackgroundJobServer` instances in the same AppDomain without explicit server names.
+* Added – `JobActivatorScope` class to support custom IoC scopes/lifetimes.
+* Added – Allow to specify a custom queue for recurring jobs (by @maleet).
+* Added – Allow to use CancellationToken type as a job method parameter.
+* Added – It is now possible to query the storage in state applying filters.
+* Added – Allow to pass custom filters to `BackgroundJobServer` via options.
+* Added – Option to hide the "Back to site" link (by @dradovic).
+* Changed – Server components and background processes now use `Task` with `TaskCreationOptions.LongRunning`.
+* Changed – ServerIds are now generated without ServerName, server names are deprecated.
+* Changed – Arguments Renderer now supports TimeSpan and DateTime (by @yngndrw).
+* Changed – Minor improvements for Dashboard navigation (by @pwelter34).
+* Changed – Worker does not process a job when a transition to the Processing state failed (by @yngndrw).
+* Changed – `BackgroundJobClientException` is thrown instead of `CreateJobFailedException`.
+* Changed – WorkerId is now auto-generated and based on a GUID.
+* Changed – Use StackTraceFormatter package for marking-up stack traces (by @atifaziz).
+* Changed – Updated Dashboard CSS to Twitter Bootstrap 3.3.5.
+* Fixed – Prevent worker from fetching and processing a job on shutdown.
+* Fixed – `TaskCanceledException` thrown from a job method leads to failed job as expected.
+* Fixed – Make `DelayedJobScheduler` resilient to empty background job ids.
+* Fixed – Logical error in SqlServerDistributedLockTimeout value check (by @Tom-Kelly).
+* Fixed – `OverflowException` in Dashboard when job duration exceeds int.MaxValue (by @mavenius).
+* Fixed – "More details" button in Dashboard changes to "Less details" when collapsed (by @pascalberger).
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/nuspecs/Hangfire.Raven.nuspec b/nuspecs/Hangfire.Raven.nuspec
new file mode 100644
index 0000000..5227744
--- /dev/null
+++ b/nuspecs/Hangfire.Raven.nuspec
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/nuspecs/Hangfire.SqlServer.MSMQ.nuspec b/nuspecs/Hangfire.SqlServer.MSMQ.nuspec
new file mode 100644
index 0000000..62791c8
--- /dev/null
+++ b/nuspecs/Hangfire.SqlServer.MSMQ.nuspec
@@ -0,0 +1,35 @@
+
+
+
+ Hangfire.SqlServer.MSMQ
+ 0.0.0
+ Hangfire MSMQ Queues for SQL Server Storage
+ Sergey Odinokov
+ odinserj
+ http://hangfire.io/
+ false
+ https://raw.github.com/HangfireIO/Hangfire/master/LICENSE.md
+ MSMQ queues support for SQL Server job storage implementation for Hangfire (background job system for ASP.NET applications).
+ Copyright © 2014-2015 Sergey Odinokov
+ Hangfire SqlServer MSMQ
+ http://hangfire.io/blog/
+
+1.5.0
+* Added – Support for remote MSMQ queues through DTC transactions.
+
+1.4.0
+* Changed – Speed up `GetCount` method with native implementation.
+* Fixed – Incorrect order of fetching when multiple queues used.
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/nuspecs/Hangfire.SqlServer.RabbitMQ.nuspec b/nuspecs/Hangfire.SqlServer.RabbitMQ.nuspec
new file mode 100644
index 0000000..5a7f2af
--- /dev/null
+++ b/nuspecs/Hangfire.SqlServer.RabbitMQ.nuspec
@@ -0,0 +1,35 @@
+
+
+
+ Hangfire.SqlServer.RabbitMQ
+ 0.0.0
+ Hangfire RabbitMQ Queues for SQL Server Storage
+ Denny Ferrassoli
+ dennyferra
+ http://hangfire.io/
+ false
+ https://raw.github.com/HangfireIO/Hangfire/master/LICENSE.md
+ RabbitMQ queues support for SQL Server job storage implementation for Hangfire (background job system for ASP.NET applications).
+ Copyright © 2014 Denny Ferrassoli
+ Hangfire SqlServer RabbitMQ
+ http://hangfire.io/blog/
+
+1.4.2
+* Fixed – Incompatibility issue with RabbitMQ.Client >= 3.4.0 (by @justmara).
+
+1.4.0
+* Added – Allow a URI to be used for `RabbitMQConnectionConfiguration` (by @dennyferra).
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/nuspecs/Hangfire.nuspec b/nuspecs/Hangfire.nuspec
new file mode 100644
index 0000000..64d7384
--- /dev/null
+++ b/nuspecs/Hangfire.nuspec
@@ -0,0 +1,71 @@
+
+
+
+ Hangfire
+ 0.0.0
+ Hangfire
+ Sergey Odinokov
+ odinserj
+ http://hangfire.io/
+ false
+ https://raw.github.com/HangfireIO/Hangfire/master/LICENSE.md
+
+ An easy and reliable way to perform fire-and-forget, delayed and recurring long-running, short-running, CPU or I/O intensive tasks inside ASP.NET application. No Windows Service / Task Scheduler required. Even ASP.NET is not required.
+ Backed by Redis, SQL Server, SQL Azure or MSMQ. This is a .NET alternative to Sidekiq, Resque and Celery.
+ http://hangfire.io/
+
+ Copyright © 2013-2015 Sergey Odinokov
+ Hangfire AspNet MVC OWIN SqlServer Long-Running Background Fire-And-Forget Delayed Recurring Tasks Jobs Scheduler Threading Queues
+ http://hangfire.io/blog/
+
+1.5.0
+
+Hangfire.Core
+* Added – `IBackgroundProcess` interface that allows to implement custom background processes.
+* Added – `BackgroundProcessingServer` class for creating a server instance with completely custom background processes.
+* Added – Support for running multiple `BackgroundJobServer` instances in the same AppDomain without explicit server names.
+* Added – `JobActivatorScope` class to support custom IoC scopes/lifetimes.
+* Added – Allow to specify a custom queue for recurring jobs (by @maleet).
+* Added – Allow to use CancellationToken type as a job method parameter.
+* Added – It is now possible to query the storage in state applying filters.
+* Added – Allow to pass custom filters to `BackgroundJobServer` via options.
+* Added – Option to hide the "Back to site" link (by @dradovic).
+* Changed – Server components and background processes now use `Task` with `TaskCreationOptions.LongRunning`.
+* Changed – ServerIds are now generated without ServerName, server names are deprecated.
+* Changed – Arguments Renderer now supports TimeSpan and DateTime (by @yngndrw).
+* Changed – Minor improvements for Dashboard navigation (by @pwelter34).
+* Changed – Worker does not process a job when a transition to the Processing state failed (by @yngndrw).
+* Changed – `BackgroundJobClientException` is thrown instead of `CreateJobFailedException`.
+* Changed – WorkerId is now auto-generated and based on a GUID.
+* Changed – Use StackTraceFormatter package for marking-up stack traces (by @atifaziz).
+* Changed – Updated Dashboard CSS to Twitter Bootstrap 3.3.5.
+* Fixed – Prevent worker from fetching and processing a job on shutdown.
+* Fixed – `TaskCanceledException` thrown from a job method leads to failed job as expected.
+* Fixed – Make `DelayedJobScheduler` resilient to empty background job ids.
+* Fixed – Logical error in SqlServerDistributedLockTimeout value check (by @Tom-Kelly).
+* Fixed – `OverflowException` in Dashboard when job duration exceeds int.MaxValue (by @mavenius).
+* Fixed – "More details" button in Dashboard changes to "Less details" when collapsed (by @pascalberger).
+
+Hangfire.SqlServer
+* Added – Ability to specify the schema for SQL Server job storage (by @ryanmwright).
+* Added – `TransactionTimeout` option for configuring the `TransactionScope` timeout.
+* Added – `DashboardJobListLimit` option to prevent full table scans in Dashboard.
+* Changed – `SqlServerDistributedLock` class is now public.
+* Changed – Use `rowlock` table hint in job fetch query to prevent escalation.
+* Changed – Dramatically improved page load times in Dashboard.
+* Changed – Instant re-queue for background jobs even after ungraceful shutdown.
+* Changed – `JobQueue.Queue` and `Server.Id` column length increased to 50 and 100 respectively (by @maleet).
+
+Hangfire.SqlServer.Msmq
+* Added – Support for remote MSMQ queues through DTC transactions.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ConsoleSample/ConsoleSample.csproj b/samples/ConsoleSample/ConsoleSample.csproj
new file mode 100644
index 0000000..7f56e02
--- /dev/null
+++ b/samples/ConsoleSample/ConsoleSample.csproj
@@ -0,0 +1,86 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {C02BB718-2AE4-434C-8668-C894FF663FCE}
+ Exe
+ Properties
+ ConsoleSample
+ ConsoleSample
+ v4.5
+ 512
+ ..\
+
+ 89909a41
+
+
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+ ManagedMinimumRules.ruleset
+ false
+
+
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+ false
+
+
+
+
+
+
+ ..\..\packages\Hangfire.Core.1.5.1\lib\net45\Hangfire.Core.dll
+ True
+
+
+ ..\..\packages\Newtonsoft.Json.5.0.1\lib\net45\Newtonsoft.Json.dll
+ True
+
+
+ ..\..\packages\Owin.1.0\lib\net40\Owin.dll
+ True
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {a523c0e3-097d-4869-977f-15a717ea3e83}
+ Hangfire.Raven
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/ConsoleSample/GenericServices.cs b/samples/ConsoleSample/GenericServices.cs
new file mode 100644
index 0000000..c9f28dd
--- /dev/null
+++ b/samples/ConsoleSample/GenericServices.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace ConsoleSample
+{
+ public class GenericServices
+ {
+ public void Method(TType arg1, TMethod arg2)
+ {
+ Console.WriteLine("Arg1: {0}, Arg2: {1}", arg1, arg2);
+ }
+ }
+}
\ No newline at end of file
diff --git a/samples/ConsoleSample/Program.cs b/samples/ConsoleSample/Program.cs
new file mode 100644
index 0000000..0eaf05f
--- /dev/null
+++ b/samples/ConsoleSample/Program.cs
@@ -0,0 +1,38 @@
+using System;
+using Hangfire;
+using HangFire.Raven.Storage;
+
+namespace ConsoleSample
+{
+ public static class Program
+ {
+ public static int x = 0;
+
+ public static void Main()
+ {
+ // you can use Raven Storage and specify the connection string and database name
+ GlobalConfiguration.Configuration
+ .UseColouredConsoleLogProvider()
+ .UseRavenStorage("http://localhost:9090", "hangfire2");
+
+ // you can use Raven Embedded Storage which runs in memory!
+ //GlobalConfiguration.Configuration
+ // .UseColouredConsoleLogProvider()
+ // .UseEmbeddedRavenStorage();
+
+ //you have to create an instance of background job server at least once for background jobs to run
+ var client = new BackgroundJobServer();
+
+ BackgroundJob.Enqueue(() => Console.WriteLine("Background Job: Hello, world!"));
+ RecurringJob.AddOrUpdate(() => test(), Cron.Minutely);
+
+ Console.WriteLine("Press Enter to exit...");
+ Console.ReadLine();
+ }
+
+ public static void test()
+ {
+ Console.WriteLine($"{x++} Cron Job: Hello, world!");
+ }
+ }
+}
\ No newline at end of file
diff --git a/samples/ConsoleSample/Properties/AssemblyInfo.cs b/samples/ConsoleSample/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..1a5ee35
--- /dev/null
+++ b/samples/ConsoleSample/Properties/AssemblyInfo.cs
@@ -0,0 +1,35 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("Hangfire.Hosts.Console")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("Hangfire.Hosts.Console")]
+[assembly: AssemblyCopyright("Copyright © 2013")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("3c3451fc-663d-4e33-ab72-d92beae38a97")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/samples/ConsoleSample/Services.cs b/samples/ConsoleSample/Services.cs
new file mode 100644
index 0000000..2977d8a
--- /dev/null
+++ b/samples/ConsoleSample/Services.cs
@@ -0,0 +1,110 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.IO;
+using System.Threading;
+using Hangfire;
+
+namespace ConsoleSample
+{
+ public class Services
+ {
+ private static readonly Random _random = new Random();
+
+ public void EmptyDefault()
+ {
+ }
+
+ [Queue("critical")]
+ public void EmptyCritical()
+ {
+ }
+
+ [AutomaticRetry(Attempts = 0)]
+ public void Error()
+ {
+ Console.WriteLine("Beginning error task...");
+ throw new InvalidOperationException(null, new FileLoadException());
+ }
+
+ [Queue("critical")]
+ public void Random(int number)
+ {
+ int time;
+ lock (_random)
+ {
+ time = _random.Next(10);
+ }
+
+ if (time < 5)
+ {
+ throw new Exception();
+ }
+
+ Thread.Sleep(TimeSpan.FromSeconds(5 + time));
+ Console.WriteLine("Finished task: " + number);
+ }
+
+ public void Cancelable(int iterationCount, IJobCancellationToken token)
+ {
+ try
+ {
+ for (var i = 1; i <= iterationCount; i++)
+ {
+ Thread.Sleep(1000);
+ Console.WriteLine("Performing step {0} of {1}...", i, iterationCount);
+
+ token.ThrowIfCancellationRequested();
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ Console.WriteLine("Cancellation requested, exiting...");
+ throw;
+ }
+ }
+
+ [DisplayName("Name: {0}")]
+ public void Args(string name, int authorId, DateTime createdAt)
+ {
+ Console.WriteLine("{0}, {1}, {2}", name, authorId, createdAt);
+ }
+
+ public void Custom(int id, string[] values, CustomObject objects, DayOfWeek dayOfWeek)
+ {
+ }
+
+ public void FullArgs(
+ bool b,
+ int i,
+ char c,
+ DayOfWeek e,
+ string s,
+ TimeSpan t,
+ DateTime d,
+ CustomObject o,
+ string[] sa,
+ int[] ia,
+ long[] ea,
+ object[] na,
+ List sl)
+ {
+ }
+
+ public class CustomObject
+ {
+ public int Id { get; set; }
+ public CustomObject[] Children { get; set; }
+ }
+
+ public void Write(char character)
+ {
+ Console.Write(character);
+ }
+
+ public void WriteBlankLine()
+ {
+ Console.WriteLine();
+ }
+ }
+}
\ No newline at end of file
diff --git a/samples/ConsoleSample/app.config b/samples/ConsoleSample/app.config
new file mode 100644
index 0000000..a7c7bf1
--- /dev/null
+++ b/samples/ConsoleSample/app.config
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ConsoleSample/packages.config b/samples/ConsoleSample/packages.config
new file mode 100644
index 0000000..fbf019f
--- /dev/null
+++ b/samples/ConsoleSample/packages.config
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/MvcSample/App_Start/FilterConfig.cs b/samples/MvcSample/App_Start/FilterConfig.cs
new file mode 100644
index 0000000..9f2eaee
--- /dev/null
+++ b/samples/MvcSample/App_Start/FilterConfig.cs
@@ -0,0 +1,12 @@
+using System.Web.Mvc;
+
+namespace MvcSample
+{
+ public class FilterConfig
+ {
+ public static void RegisterGlobalFilters(GlobalFilterCollection filters)
+ {
+ filters.Add(new HandleErrorAttribute());
+ }
+ }
+}
\ No newline at end of file
diff --git a/samples/MvcSample/App_Start/RouteConfig.cs b/samples/MvcSample/App_Start/RouteConfig.cs
new file mode 100644
index 0000000..5a3433f
--- /dev/null
+++ b/samples/MvcSample/App_Start/RouteConfig.cs
@@ -0,0 +1,19 @@
+using System.Web.Mvc;
+using System.Web.Routing;
+
+namespace MvcSample
+{
+ public class RouteConfig
+ {
+ public static void RegisterRoutes(RouteCollection routes)
+ {
+ routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
+
+ routes.MapRoute(
+ name: "Default",
+ url: "{controller}/{action}/{id}",
+ defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/samples/MvcSample/Content/Site.css b/samples/MvcSample/Content/Site.css
new file mode 100644
index 0000000..0e2ab5e
--- /dev/null
+++ b/samples/MvcSample/Content/Site.css
@@ -0,0 +1,61 @@
+body {
+ font-size: .85em;
+ font-family: "Segoe UI", Verdana, Helvetica, Sans-Serif;
+ color: #232323;
+ background-color: #fff;
+}
+
+header, footer, nav, section {
+ display: block;
+}
+
+
+/* Styles for basic forms
+-----------------------------------------------------------*/
+fieldset {
+ border: 1px solid #ddd;
+ padding: 0 1.4em 1.4em 1.4em;
+ margin: 0 0 1.5em 0;
+}
+
+legend {
+ font-size: 1.2em;
+ font-weight: bold;
+}
+
+textarea {
+ min-height: 75px;
+}
+
+.editor-label {
+ margin: 1em 0 0 0;
+}
+
+.editor-field {
+ margin: 0.5em 0 0 0;
+}
+
+
+/* Styles for validation helpers
+-----------------------------------------------------------*/
+.field-validation-error {
+ color: #f00;
+}
+
+.field-validation-valid {
+ display: none;
+}
+
+.input-validation-error {
+ border: 1px solid #f00;
+ background-color: #fee;
+}
+
+.validation-summary-errors {
+ font-weight: bold;
+ color: #f00;
+}
+
+.validation-summary-valid {
+ display: none;
+}
diff --git a/samples/MvcSample/Global.asax b/samples/MvcSample/Global.asax
new file mode 100644
index 0000000..ea55977
--- /dev/null
+++ b/samples/MvcSample/Global.asax
@@ -0,0 +1 @@
+<%@ Application Codebehind="Global.asax.cs" Inherits="MvcSample.MvcApplication" Language="C#" %>
diff --git a/samples/MvcSample/Global.asax.cs b/samples/MvcSample/Global.asax.cs
new file mode 100644
index 0000000..877a84b
--- /dev/null
+++ b/samples/MvcSample/Global.asax.cs
@@ -0,0 +1,19 @@
+using System.Web.Mvc;
+using System.Web.Routing;
+
+namespace MvcSample
+{
+ // Note: For instructions on enabling IIS6 or IIS7 classic mode,
+ // visit http://go.microsoft.com/?LinkId=9394801
+
+ public class MvcApplication : System.Web.HttpApplication
+ {
+ protected void Application_Start()
+ {
+ AreaRegistration.RegisterAllAreas();
+
+ FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
+ RouteConfig.RegisterRoutes(RouteTable.Routes);
+ }
+ }
+}
\ No newline at end of file
diff --git a/samples/MvcSample/MvcSample.csproj b/samples/MvcSample/MvcSample.csproj
new file mode 100644
index 0000000..ce8e538
--- /dev/null
+++ b/samples/MvcSample/MvcSample.csproj
@@ -0,0 +1,203 @@
+
+
+
+
+ Debug
+ AnyCPU
+
+
+ 2.0
+ {FE5EAE49-2627-42B6-8C2B-EBE4D45B4D97}
+ {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc}
+ Library
+ Properties
+ MvcSample
+ MvcSample
+ v4.5
+ false
+ true
+
+
+
+
+ ..\
+ true
+
+
+ C:\Users\odinserj\Documents\GitHub\Hangfire\Backup6\
+ 4.0
+
+
+
+ 5fc7aa54
+
+
+ true
+ full
+ false
+ bin\
+ DEBUG;TRACE
+ prompt
+ 4
+ ManagedMinimumRules.ruleset
+
+
+ pdbonly
+ true
+ bin\
+ TRACE
+ prompt
+ 4
+
+
+
+
+ ..\..\packages\Microsoft.Owin.3.0.1\lib\net45\Microsoft.Owin.dll
+ True
+
+
+ ..\..\packages\Microsoft.Owin.Host.SystemWeb.3.0.1\lib\net45\Microsoft.Owin.Host.SystemWeb.dll
+ True
+
+
+ ..\..\packages\Owin.1.0\lib\net40\Owin.dll
+
+
+
+
+
+
+ ..\..\packages\Microsoft.Net.Http.2.2.13\lib\net45\System.Net.Http.Extensions.dll
+
+
+ ..\..\packages\Microsoft.Net.Http.2.2.13\lib\net45\System.Net.Http.Primitives.dll
+
+
+
+
+
+
+
+
+ ..\..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.Helpers.dll
+ True
+
+
+ ..\..\packages\Microsoft.AspNet.Mvc.5.2.3\lib\net45\System.Web.Mvc.dll
+ True
+
+
+ ..\..\packages\Microsoft.AspNet.Razor.3.2.3\lib\net45\System.Web.Razor.dll
+ True
+
+
+ ..\..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.dll
+ True
+
+
+ ..\..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.Deployment.dll
+ True
+
+
+ ..\..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.Razor.dll
+ True
+
+
+
+
+
+
+
+
+
+
+
+ True
+ ..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll
+
+
+
+
+
+
+
+
+
+
+ Global.asax
+
+
+
+
+
+
+
+
+
+ Web.config
+
+
+ Web.config
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {a523c0e3-097d-4869-977f-15a717ea3e83}
+ Hangfire.Raven
+
+
+ {c02bb718-2ae4-434c-8668-c894ff663fce}
+ ConsoleSample
+
+
+
+
+
+
+
+
+
+
+
+
+ True
+ True
+ 0
+ /
+ http://localhost:56909/
+ False
+ False
+
+
+ False
+
+
+
+
+
+
+
+ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/MvcSample/MvcSample.csproj.DotSettings b/samples/MvcSample/MvcSample.csproj.DotSettings
new file mode 100644
index 0000000..07d5ce7
--- /dev/null
+++ b/samples/MvcSample/MvcSample.csproj.DotSettings
@@ -0,0 +1,2 @@
+
+ True
\ No newline at end of file
diff --git a/samples/MvcSample/Properties/AssemblyInfo.cs b/samples/MvcSample/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..ea6b465
--- /dev/null
+++ b/samples/MvcSample/Properties/AssemblyInfo.cs
@@ -0,0 +1,34 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("Hangfire.MvcSample")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("Hangfire.MvcSample")]
+[assembly: AssemblyCopyright("Copyright © 2013")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("1579df48-9733-47a5-93b6-8b459e9912fc")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Revision and Build Numbers
+// by using the '*' as shown below:
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/samples/MvcSample/Startup.cs b/samples/MvcSample/Startup.cs
new file mode 100644
index 0000000..effc968
--- /dev/null
+++ b/samples/MvcSample/Startup.cs
@@ -0,0 +1,26 @@
+using Hangfire;
+using Hangfire.Dashboard;
+using Hangfire.SqlServer;
+using Microsoft.Owin;
+using MvcSample;
+using Owin;
+
+[assembly: OwinStartup(typeof(Startup))]
+
+namespace MvcSample
+{
+ public class Startup
+ {
+ public void Configuration(IAppBuilder app)
+ {
+ GlobalConfiguration.Configuration
+ .UseSqlServerStorage(@"Server=.\sqlexpress;Database=Hangfire.Sample;Trusted_Connection=True;")
+ .UseMsmqQueues(@".\Private$\hangfire{0}", "default", "critical")
+ .UseDashboardMetric(SqlServerStorage.ActiveConnections)
+ .UseDashboardMetric(SqlServerStorage.TotalConnections)
+ .UseDashboardMetric(DashboardMetrics.FailedCount);
+
+ app.UseHangfireDashboard();
+ }
+ }
+}
diff --git a/samples/MvcSample/Views/Shared/Error.cshtml b/samples/MvcSample/Views/Shared/Error.cshtml
new file mode 100644
index 0000000..b4a1239
--- /dev/null
+++ b/samples/MvcSample/Views/Shared/Error.cshtml
@@ -0,0 +1,17 @@
+@{
+ Layout = null;
+}
+
+
+
+
+
+ Error
+
+
+
+ Error.
+ An error occurred while processing your request.
+
+
+
\ No newline at end of file
diff --git a/samples/MvcSample/Views/Shared/_Layout.cshtml b/samples/MvcSample/Views/Shared/_Layout.cshtml
new file mode 100644
index 0000000..cdd3281
--- /dev/null
+++ b/samples/MvcSample/Views/Shared/_Layout.cshtml
@@ -0,0 +1,16 @@
+
+
+
+
+
+ @ViewBag.Title
+ @Styles.Render("~/Content/css")
+ @Scripts.Render("~/bundles/modernizr")
+
+
+ @RenderBody()
+
+ @Scripts.Render("~/bundles/jquery")
+ @RenderSection("scripts", required: false)
+
+
diff --git a/samples/MvcSample/Views/Web.config b/samples/MvcSample/Views/Web.config
new file mode 100644
index 0000000..826ce19
--- /dev/null
+++ b/samples/MvcSample/Views/Web.config
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/MvcSample/Views/_ViewStart.cshtml b/samples/MvcSample/Views/_ViewStart.cshtml
new file mode 100644
index 0000000..efda124
--- /dev/null
+++ b/samples/MvcSample/Views/_ViewStart.cshtml
@@ -0,0 +1,3 @@
+@{
+ Layout = "~/Views/Shared/_Layout.cshtml";
+}
\ No newline at end of file
diff --git a/samples/MvcSample/Web.Debug.config b/samples/MvcSample/Web.Debug.config
new file mode 100644
index 0000000..3e2a97c
--- /dev/null
+++ b/samples/MvcSample/Web.Debug.config
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/MvcSample/Web.Release.config b/samples/MvcSample/Web.Release.config
new file mode 100644
index 0000000..9fd481f
--- /dev/null
+++ b/samples/MvcSample/Web.Release.config
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/MvcSample/Web.config b/samples/MvcSample/Web.config
new file mode 100644
index 0000000..203dc2a
--- /dev/null
+++ b/samples/MvcSample/Web.config
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/MvcSample/packages.config b/samples/MvcSample/packages.config
new file mode 100644
index 0000000..72f6ff2
--- /dev/null
+++ b/samples/MvcSample/packages.config
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Hangfire.SqlServer/DistributedLocks/RavenDistributedLock.cs b/src/Hangfire.SqlServer/DistributedLocks/RavenDistributedLock.cs
new file mode 100644
index 0000000..e1b7b84
--- /dev/null
+++ b/src/Hangfire.SqlServer/DistributedLocks/RavenDistributedLock.cs
@@ -0,0 +1,208 @@
+// This file is part of Hangfire.
+// Copyright � 2013-2014 Sergey Odinokov.
+//
+// Hangfire is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation, either version 3
+// of the License, or any later version.
+//
+// Hangfire is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with Hangfire. If not, see .
+
+using System;
+using System.Linq;
+using System.Collections.Generic;
+using System.Data;
+using System.Threading;
+using Hangfire.Annotations;
+using HangFire.Raven;
+using Hangfire.Raven.Entities;
+using Hangfire.Raven.Storage;
+
+namespace Hangfire.Raven.DistributedLock
+{
+ public class RavenDistributedLock : IDisposable
+ {
+ private const string LockMode = "Exclusive";
+ private const string LockOwner = "Session";
+ private const int CommandTimeoutAdditionSeconds = 1;
+
+ private static readonly IDictionary LockErrorMessages = new Dictionary
+ {
+ { -1, "The lock request timed out" },
+ { -2, "The lock request was canceled" },
+ { -3, "The lock request was chosen as a deadlock victim" },
+ { -999, "Indicates a parameter validation or other call error" }
+ };
+
+ private static readonly ThreadLocal> AcquiredLocks = new ThreadLocal>(() => new Dictionary());
+
+ private static RavenStorage _storage;
+ private static string _resource;
+ private static RavenStorageOptions _options;
+
+ private static Timer _heartbeatTimer = null;
+
+ private bool _completed;
+
+ public RavenDistributedLock([NotNull] RavenStorage storage, [NotNull] string resource, TimeSpan timeout, RavenStorageOptions options)
+ {
+ storage.ThrowIfNull("storage");
+ resource.ThrowIfNull("resource");
+
+ if ((timeout.TotalSeconds + CommandTimeoutAdditionSeconds) > int.MaxValue) {
+ throw new ArgumentException(string.Format("The timeout specified is too large. Please supply a timeout equal to or less than {0} seconds",
+ int.MaxValue - CommandTimeoutAdditionSeconds),
+ "timeout");
+ }
+
+ _storage = storage;
+ _resource = resource;
+ _options = options;
+
+ if (!AcquiredLocks.Value.ContainsKey(_resource)) {
+ Acquire(_resource, timeout);
+ AcquiredLocks.Value[_resource] = 1;
+ } else {
+ AcquiredLocks.Value[_resource]++;
+ }
+ }
+
+ public void Dispose()
+ {
+ if (_completed)
+ return;
+
+ _completed = true;
+
+ if (!AcquiredLocks.Value.ContainsKey(_resource))
+ return;
+
+ AcquiredLocks.Value[_resource]--;
+
+ if (AcquiredLocks.Value[_resource] != 0)
+ return;
+
+ Release(_resource);
+ AcquiredLocks.Value.Remove(_resource);
+ }
+
+ internal static void Acquire(string resource, TimeSpan timeout)
+ {
+ try {
+ RemoveDeadLocks();
+
+ // Check lock
+ DateTime lockTimeoutTime = DateTime.Now.Add(timeout);
+ bool isLockedBySomeoneElse;
+ bool isFirstAttempt = true;
+ do {
+ using (var repository = new Repository()) {
+ isLockedBySomeoneElse = repository.Session.Query()
+ .FirstOrDefault(t => t.Resource == resource && t.ClientId == _options.ClientId) != null;
+ }
+
+ if (isFirstAttempt == true) {
+ isFirstAttempt = false;
+ } else {
+ Thread.Sleep((int)timeout.TotalMilliseconds / 10);
+ }
+ }
+ while ((isLockedBySomeoneElse == true) && (lockTimeoutTime >= DateTime.Now));
+
+ // Set lock
+ if (isLockedBySomeoneElse == false) {
+ using (var repository = new Repository()) {
+ var distributedLocks = repository.Session.Query().Where(t => t.Resource == resource).ToList();
+
+ if (!distributedLocks.Any()) {
+ distributedLocks.Add(new DistributedLocks
+ {
+ Resource = resource,
+ });
+ }
+
+ foreach (var distributedLock in distributedLocks) {
+ distributedLock.ClientId = _options.ClientId;
+ distributedLock.LockCount = 1;
+ distributedLock.Heartbeat = DateTime.UtcNow;
+
+ repository.Save(distributedLock);
+ }
+ }
+
+ StartHeartBeat();
+ } else {
+ throw new RavenDistributedLockException(string.Format("Could not place a lock on the resource '{0}': {1}.", _resource, "The lock request timed out"));
+ }
+ } catch (Exception ex) {
+ if (ex is RavenDistributedLockException) {
+ throw;
+ } else {
+ throw new RavenDistributedLockException(string.Format("Could not place a lock on the resource '{0}': {1}.", _resource, "Check inner exception for details"), ex);
+ }
+ }
+ }
+
+ internal static void Release(string resource)
+ {
+ try {
+ RemoveDeadLocks();
+
+ // Remove resource lock
+ using (var repository = new Repository()) {
+ var distributedLocks = repository.Session.Query().Where(t => t.Resource == _resource && t.ClientId == _options.ClientId).ToList();
+
+ foreach (var distributedLock in distributedLocks) {
+ repository.Delete(distributedLock);
+ }
+ }
+
+ if (_heartbeatTimer != null) {
+ _heartbeatTimer.Dispose();
+ _heartbeatTimer = null;
+ }
+ } catch (Exception ex) {
+ throw new RavenDistributedLockException(string.Format("Could not release a lock on the resource '{0}': {1}.", _resource, "Check inner exception for details"), ex);
+ }
+ }
+
+ private static void StartHeartBeat()
+ {
+ TimeSpan timerInterval = TimeSpan.FromMilliseconds(_options.DistributedLockLifetime.TotalMilliseconds / 5);
+
+ _heartbeatTimer = new Timer(state =>
+ {
+ try {
+ using (var repository = new Repository()) {
+ var distributedLocks = repository.Session.Query().Where(t => t.Resource == _resource && t.ClientId == _options.ClientId).ToList();
+
+ foreach (var distributedLock in distributedLocks) {
+ distributedLock.Heartbeat = DateTime.UtcNow;
+ repository.Save(distributedLock);
+ }
+ }
+ } catch (Exception ex) {
+ Console.WriteLine("Unable to update heartbeat on the resource '{0}'", ex, _resource);
+ }
+ }, null, timerInterval, timerInterval);
+ }
+
+ private static void RemoveDeadLocks()
+ {
+ using (var repository = new Repository()) {
+ var heartBeat = DateTime.UtcNow.Subtract(_options.DistributedLockLifetime);
+ var deadLocks = repository.Session.Query().Where(t => t.Resource == _resource && t.Heartbeat == heartBeat).ToList();
+
+ foreach (var deadlock in deadLocks) {
+ repository.Delete(deadlock);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Hangfire.SqlServer/DistributedLocks/RavenDistributedLockException.cs b/src/Hangfire.SqlServer/DistributedLocks/RavenDistributedLockException.cs
new file mode 100644
index 0000000..9771862
--- /dev/null
+++ b/src/Hangfire.SqlServer/DistributedLocks/RavenDistributedLockException.cs
@@ -0,0 +1,30 @@
+using System;
+
+namespace Hangfire.Raven.DistributedLock
+{
+ ///
+ /// Represents exceptions for distributed lock implementation for MongoDB
+ ///
+ [Serializable]
+ public class RavenDistributedLockException : Exception
+ {
+ ///
+ /// Creates exception
+ ///
+ /// Exception message
+ public RavenDistributedLockException(string message)
+ : base(message)
+ {
+ }
+
+ ///
+ /// Creates exception with inner exception
+ ///
+ /// Exception message
+ /// Inner exception
+ public RavenDistributedLockException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Hangfire.SqlServer/Entities/AggregatedCounter.cs b/src/Hangfire.SqlServer/Entities/AggregatedCounter.cs
new file mode 100644
index 0000000..af03256
--- /dev/null
+++ b/src/Hangfire.SqlServer/Entities/AggregatedCounter.cs
@@ -0,0 +1,15 @@
+using System;
+
+namespace Hangfire.Raven.Entities
+{
+ public class AggregatedCounter
+ {
+ public string Id { get; set; }
+
+ public string Key { get; set; }
+
+ public long Value { get; set; }
+
+ public DateTime? ExpireAt { get; set; }
+ }
+}
diff --git a/src/Hangfire.SqlServer/Entities/Counter.cs b/src/Hangfire.SqlServer/Entities/Counter.cs
new file mode 100644
index 0000000..8f18b88
--- /dev/null
+++ b/src/Hangfire.SqlServer/Entities/Counter.cs
@@ -0,0 +1,18 @@
+using Hangfire.Raven.Entities.Identity;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Hangfire.Raven.Entities
+{
+ public class Counter : BaseEntity
+ {
+ public string Key { get; set; }
+
+ public int Value { get; set; }
+
+ public DateTime? ExpireAt { get; set; }
+ }
+}
diff --git a/src/Hangfire.SqlServer/Entities/DistributedLocks.cs b/src/Hangfire.SqlServer/Entities/DistributedLocks.cs
new file mode 100644
index 0000000..511a325
--- /dev/null
+++ b/src/Hangfire.SqlServer/Entities/DistributedLocks.cs
@@ -0,0 +1,16 @@
+using Hangfire.Raven.Entities.Identity;
+using System;
+
+namespace Hangfire.Raven.Entities
+{
+ public class DistributedLocks : BaseEntity
+ {
+ public string Resource { get; set; }
+
+ public string ClientId { get; set; }
+
+ public int LockCount { get; set; }
+
+ public DateTime Heartbeat { get; set; }
+ }
+}
diff --git a/src/Hangfire.SqlServer/Entities/EnqueuedAndFetchedCount.cs b/src/Hangfire.SqlServer/Entities/EnqueuedAndFetchedCount.cs
new file mode 100644
index 0000000..8f76d57
--- /dev/null
+++ b/src/Hangfire.SqlServer/Entities/EnqueuedAndFetchedCount.cs
@@ -0,0 +1,9 @@
+namespace Hangfire.Raven.Entities
+{
+ public class EnqueuedAndFetchedCount
+ {
+ public int? EnqueuedCount { get; set; }
+
+ public int? FetchedCount { get; set; }
+ }
+}
diff --git a/src/Hangfire.SqlServer/Entities/Identity/BaseEntity.cs b/src/Hangfire.SqlServer/Entities/Identity/BaseEntity.cs
new file mode 100644
index 0000000..09173df
--- /dev/null
+++ b/src/Hangfire.SqlServer/Entities/Identity/BaseEntity.cs
@@ -0,0 +1,7 @@
+namespace Hangfire.Raven.Entities.Identity
+{
+ public class BaseEntity
+ {
+ public string Id { get; set; }
+ }
+}
diff --git a/src/Hangfire.SqlServer/Entities/JobParameter.cs b/src/Hangfire.SqlServer/Entities/JobParameter.cs
new file mode 100644
index 0000000..608ac0f
--- /dev/null
+++ b/src/Hangfire.SqlServer/Entities/JobParameter.cs
@@ -0,0 +1,27 @@
+// This file is part of Hangfire.
+// Copyright © 2013-2014 Sergey Odinokov.
+//
+// Hangfire is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation, either version 3
+// of the License, or any later version.
+//
+// Hangfire is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with Hangfire. If not, see .
+
+using Hangfire.Raven.Entities.Identity;
+
+namespace Hangfire.Raven.Entities
+{
+ internal class JobParameter : BaseEntity
+ {
+ public string JobId { get; set; }
+ public string Name { get; set; }
+ public string Value { get; set; }
+ }
+}
diff --git a/src/Hangfire.SqlServer/Entities/JobQueue.cs b/src/Hangfire.SqlServer/Entities/JobQueue.cs
new file mode 100644
index 0000000..e849048
--- /dev/null
+++ b/src/Hangfire.SqlServer/Entities/JobQueue.cs
@@ -0,0 +1,12 @@
+using Hangfire.Raven.Entities.Identity;
+using System;
+
+namespace Hangfire.Raven.Entities
+{
+ public class JobQueue : BaseEntity
+ {
+ public string JobId { get; set; }
+ public string Queue { get; set; }
+ public DateTime? FetchedAt { get; set; }
+ }
+}
diff --git a/src/Hangfire.SqlServer/Entities/RavenFetchedJob.cs b/src/Hangfire.SqlServer/Entities/RavenFetchedJob.cs
new file mode 100644
index 0000000..6bbd59b
--- /dev/null
+++ b/src/Hangfire.SqlServer/Entities/RavenFetchedJob.cs
@@ -0,0 +1,88 @@
+// This file is part of Hangfire.
+// Copyright © 2013-2014 Sergey Odinokov.
+//
+// Hangfire is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation, either version 3
+// of the License, or any later version.
+//
+// Hangfire is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with Hangfire. If not, see .
+
+using Hangfire.Annotations;
+using Hangfire.Storage;
+using HangFire.Raven;
+using Hangfire.Raven.Storage;
+
+namespace Hangfire.Raven.Entities
+{
+ public class RavenFetchedJob : IFetchedJob
+ {
+ private readonly RavenStorage _storage;
+ private bool _requeued { get; set; }
+ private bool _removedFromQueue { get; set; }
+ private bool _disposed { get; set; }
+
+ public string Id { get; set; }
+ public string JobId { get; set; }
+ public string Queue { get; set; }
+
+ public RavenFetchedJob(
+ [NotNull] RavenStorage storage,
+ JobQueue jobQueue)
+ {
+ storage.ThrowIfNull("storage");
+ jobQueue.ThrowIfNull("jobQueue");
+
+ _storage = storage;
+
+ JobId = jobQueue.JobId;
+ Queue = jobQueue.Queue;
+ Id = jobQueue.Id;
+ }
+
+ public void RemoveFromQueue()
+ {
+ using (var repository = new Repository()) {
+ var job = repository.Session.Load(Id);
+
+ if (job != null) {
+ repository.Delete(job);
+ }
+ }
+
+ _removedFromQueue = true;
+ }
+
+ public void Requeue()
+ {
+ using (var repository = new Repository()) {
+ var job = repository.Session.Load(Id);
+
+ job.FetchedAt = null;
+
+ repository.Save(job);
+ }
+
+ _requeued = true;
+ }
+
+ public void Dispose()
+ {
+ if (_disposed) {
+ return;
+ }
+
+ if (!_removedFromQueue && !_requeued) {
+ Requeue();
+ }
+
+ _disposed = true;
+ }
+ }
+}
diff --git a/src/Hangfire.SqlServer/Entities/RavenHash.cs b/src/Hangfire.SqlServer/Entities/RavenHash.cs
new file mode 100644
index 0000000..ce01c7b
--- /dev/null
+++ b/src/Hangfire.SqlServer/Entities/RavenHash.cs
@@ -0,0 +1,31 @@
+// This file is part of Hangfire.
+// Copyright © 2013-2014 Sergey Odinokov.
+//
+// Hangfire is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation, either version 3
+// of the License, or any later version.
+//
+// Hangfire is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with Hangfire. If not, see .
+
+using Hangfire.Raven.Entities.Identity;
+using System;
+
+namespace Hangfire.Raven.Entities
+{
+ public class RavenHash : BaseEntity
+ {
+ public string Key { get; set; }
+ public string Field { get; set; }
+ public string Value { get; set; }
+ public DateTime? ExpireAt { get; set; }
+
+ public string JobId { get; set; }
+ }
+}
diff --git a/src/Hangfire.SqlServer/Entities/RavenJob.cs b/src/Hangfire.SqlServer/Entities/RavenJob.cs
new file mode 100644
index 0000000..d8a174d
--- /dev/null
+++ b/src/Hangfire.SqlServer/Entities/RavenJob.cs
@@ -0,0 +1,34 @@
+// This file is part of Hangfire.
+// Copyright © 2013-2014 Sergey Odinokov.
+//
+// Hangfire is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation, either version 3
+// of the License, or any later version.
+//
+// Hangfire is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with Hangfire. If not, see .
+
+using Hangfire.Raven.Entities.Identity;
+using System;
+
+namespace Hangfire.Raven.Entities
+{
+ internal class RavenJob : BaseEntity
+ {
+ public string InvocationData { get; set; }
+ public string Arguments { get; set; }
+ public DateTime CreatedAt { get; set; }
+ public DateTime? ExpireAt { get; set; }
+
+ public string StateId { get; set; }
+ public string StateName { get; set; }
+ public string StateReason { get; set; }
+ public string StateData { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Hangfire.SqlServer/Entities/RavenList.cs b/src/Hangfire.SqlServer/Entities/RavenList.cs
new file mode 100644
index 0000000..cefef85
--- /dev/null
+++ b/src/Hangfire.SqlServer/Entities/RavenList.cs
@@ -0,0 +1,14 @@
+using Hangfire.Raven.Entities.Identity;
+using System;
+
+namespace Hangfire.Raven.Entities
+{
+ public class RavenList : BaseEntity
+ {
+ public string Key { get; set; }
+
+ public string Value { get; set; }
+
+ public DateTime ExpireAt { get; set; }
+ }
+}
diff --git a/src/Hangfire.SqlServer/Entities/RavenServer.cs b/src/Hangfire.SqlServer/Entities/RavenServer.cs
new file mode 100644
index 0000000..b855f57
--- /dev/null
+++ b/src/Hangfire.SqlServer/Entities/RavenServer.cs
@@ -0,0 +1,27 @@
+// This file is part of Hangfire.
+// Copyright © 2013-2014 Sergey Odinokov.
+//
+// Hangfire is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation, either version 3
+// of the License, or any later version.
+//
+// Hangfire is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with Hangfire. If not, see .
+
+using Hangfire.Raven.Entities.Identity;
+using System;
+
+namespace Hangfire.Raven.Entities
+{
+ public class RavenServer : BaseEntity
+ {
+ public string Data { get; set; }
+ public DateTime LastHeartbeat { get; set; }
+ }
+}
diff --git a/src/Hangfire.SqlServer/Entities/RavenSet.cs b/src/Hangfire.SqlServer/Entities/RavenSet.cs
new file mode 100644
index 0000000..a04d039
--- /dev/null
+++ b/src/Hangfire.SqlServer/Entities/RavenSet.cs
@@ -0,0 +1,16 @@
+using Hangfire.Raven.Entities.Identity;
+using System;
+
+namespace Hangfire.Raven.Entities
+{
+ public class RavenSet : BaseEntity
+ {
+ public string Key { get; set; }
+
+ public double Score { get; set; }
+
+ public string Value { get; set; }
+
+ public DateTime? ExpireAt { get; set; }
+ }
+}
diff --git a/src/Hangfire.SqlServer/Entities/ServerData.cs b/src/Hangfire.SqlServer/Entities/ServerData.cs
new file mode 100644
index 0000000..b58338c
--- /dev/null
+++ b/src/Hangfire.SqlServer/Entities/ServerData.cs
@@ -0,0 +1,27 @@
+// This file is part of Hangfire.
+// Copyright © 2013-2014 Sergey Odinokov.
+//
+// Hangfire is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation, either version 3
+// of the License, or any later version.
+//
+// Hangfire is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with Hangfire. If not, see .
+
+using System;
+
+namespace Hangfire.Raven.Entities
+{
+ public class ServerData
+ {
+ public int WorkerCount { get; set; }
+ public string[] Queues { get; set; }
+ public DateTime? StartedAt { get; set; }
+ }
+}
diff --git a/src/Hangfire.SqlServer/Entities/State.cs b/src/Hangfire.SqlServer/Entities/State.cs
new file mode 100644
index 0000000..45ef3bc
--- /dev/null
+++ b/src/Hangfire.SqlServer/Entities/State.cs
@@ -0,0 +1,30 @@
+// This file is part of Hangfire.
+// Copyright © 2013-2014 Sergey Odinokov.
+//
+// Hangfire is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation, either version 3
+// of the License, or any later version.
+//
+// Hangfire is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with Hangfire. If not, see .
+
+using Hangfire.Raven.Entities.Identity;
+using System;
+
+namespace Hangfire.Raven.Entities
+{
+ public class State : BaseEntity
+ {
+ public string JobId { get; set; }
+ public string Name { get; set; }
+ public string Reason { get; set; }
+ public DateTime CreatedAt { get; set; }
+ public string Data { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Hangfire.SqlServer/ExpirationManager.cs b/src/Hangfire.SqlServer/ExpirationManager.cs
new file mode 100644
index 0000000..ba8775f
--- /dev/null
+++ b/src/Hangfire.SqlServer/ExpirationManager.cs
@@ -0,0 +1,74 @@
+// This file is part of Hangfire.
+// Copyright © 2013-2014 Sergey Odinokov.
+//
+// Hangfire is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation, either version 3
+// of the License, or any later version.
+//
+// Hangfire is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with Hangfire. If not, see .
+
+using System;
+using System.Threading;
+using Hangfire.Logging;
+using Hangfire.Server;
+using System.Linq;
+using HangFire.Raven;
+using Hangfire.Raven.Entities;
+
+namespace Hangfire.Raven
+{
+ public class ExpirationManager : IServerComponent
+ {
+ private static readonly ILog Logger = LogProvider.GetCurrentClassLogger();
+
+ private readonly TimeSpan _checkInterval;
+
+ public ExpirationManager()
+ : this(TimeSpan.FromHours(1))
+ {
+ }
+
+ public ExpirationManager(TimeSpan checkInterval)
+ {
+ _checkInterval = checkInterval;
+ }
+
+ public void Execute(CancellationToken cancellationToken)
+ {
+ using (var repository = new Repository()) {
+ var session = repository.Session;
+
+ var now = DateTime.UtcNow;
+
+ var counters = session.Query().Where(t => t.ExpireAt <= now).ToList();
+ counters.ForEach(t => session.Delete(t));
+
+ var jobs = session.Query().Where(t => t.ExpireAt <= now).ToList();
+ jobs.ForEach(t => session.Delete(t));
+
+ var hashes = session.Query().Where(t => t.ExpireAt <= now).ToList();
+ hashes.ForEach(t => session.Delete(t));
+
+ var lists = session.Query().Where(t => t.ExpireAt <= now).ToList();
+ lists.ForEach(t => session.Delete(t));
+
+ var sets = session.Query().Where(t => t.ExpireAt <= now).ToList();
+ sets.ForEach(t => session.Delete(t));
+ }
+
+ cancellationToken.WaitHandle.WaitOne(_checkInterval);
+ }
+
+ public override string ToString()
+ {
+ return "Raven Expiration Manager";
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Hangfire.SqlServer/Extensions/DatabaseExtensions.cs b/src/Hangfire.SqlServer/Extensions/DatabaseExtensions.cs
new file mode 100644
index 0000000..f6339f8
--- /dev/null
+++ b/src/Hangfire.SqlServer/Extensions/DatabaseExtensions.cs
@@ -0,0 +1,14 @@
+using Raven.Client;
+
+namespace Hangfire.Raven.Extensions
+{
+ public static class DatabaseExtensions
+ {
+ public static bool DatabaseExists(this IDocumentStore documentStore, string database)
+ {
+ var result = documentStore.DatabaseCommands.Head("Raven/Databases/" + database);
+
+ return (result != null);
+ }
+ }
+}
diff --git a/src/Hangfire.SqlServer/HangFire.SqlServer.csproj.DotSettings b/src/Hangfire.SqlServer/HangFire.SqlServer.csproj.DotSettings
new file mode 100644
index 0000000..5af10f9
--- /dev/null
+++ b/src/Hangfire.SqlServer/HangFire.SqlServer.csproj.DotSettings
@@ -0,0 +1,2 @@
+
+ False
\ No newline at end of file
diff --git a/src/Hangfire.SqlServer/Hangfire.Raven.csproj b/src/Hangfire.SqlServer/Hangfire.Raven.csproj
new file mode 100644
index 0000000..8077124
--- /dev/null
+++ b/src/Hangfire.SqlServer/Hangfire.Raven.csproj
@@ -0,0 +1,150 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {A523C0E3-097D-4869-977F-15A717EA3E83}
+ Library
+ Properties
+ Hangfire.Raven
+ Hangfire.Raven
+ v4.5
+ 512
+ ..\
+
+ 5f064ca7
+
+
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+ bin\Debug\Hangfire.Raven.xml
+ false
+ 1591
+ AnyCPU
+
+
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+ bin\Release\Hangfire.Raven.xml
+ false
+ 1591
+
+
+
+ ..\..\packages\Dewey.Net.2.7.8.0\lib\net45\Dewey.Net.dll
+ True
+
+
+ ..\..\packages\Hangfire.Core.1.5.0\lib\net45\Hangfire.Core.dll
+
+
+ ..\..\packages\Hangfire.SqlServer.1.5.0\lib\net45\Hangfire.SqlServer.dll
+
+
+ ..\..\packages\Microsoft.Owin.3.0.0\lib\net45\Microsoft.Owin.dll
+
+
+ ..\..\packages\Microsoft.Owin.Host.SystemWeb.3.0.0\lib\net45\Microsoft.Owin.Host.SystemWeb.dll
+
+
+ ..\..\packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll
+ True
+
+
+ ..\..\packages\Owin.1.0\lib\net40\Owin.dll
+
+
+ ..\..\packages\RavenDB.Database.3.0.3800\lib\net45\Raven.Abstractions.dll
+
+
+ ..\..\packages\RavenDB.Client.3.0.3800\lib\net45\Raven.Client.Lightweight.dll
+
+
+ ..\..\packages\RavenDB.Database.3.0.3800\lib\net45\Raven.Database.dll
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Properties\SharedAssemblyInfo.cs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Hangfire.SqlServer/JobQueues/IPersistentJobQueue.cs b/src/Hangfire.SqlServer/JobQueues/IPersistentJobQueue.cs
new file mode 100644
index 0000000..6c8b3ca
--- /dev/null
+++ b/src/Hangfire.SqlServer/JobQueues/IPersistentJobQueue.cs
@@ -0,0 +1,27 @@
+// This file is part of Hangfire.
+// Copyright © 2013-2014 Sergey Odinokov.
+//
+// Hangfire is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation, either version 3
+// of the License, or any later version.
+//
+// Hangfire is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with Hangfire. If not, see .
+
+using System.Threading;
+using Hangfire.Storage;
+
+namespace Hangfire.Raven.JobQueues
+{
+ public interface IPersistentJobQueue
+ {
+ IFetchedJob Dequeue(string[] queues, CancellationToken cancellationToken);
+ void Enqueue(string queue, string jobId);
+ }
+}
\ No newline at end of file
diff --git a/src/Hangfire.SqlServer/JobQueues/IPersistentJobQueueMonitoringApi.cs b/src/Hangfire.SqlServer/JobQueues/IPersistentJobQueueMonitoringApi.cs
new file mode 100644
index 0000000..2e3cb02
--- /dev/null
+++ b/src/Hangfire.SqlServer/JobQueues/IPersistentJobQueueMonitoringApi.cs
@@ -0,0 +1,32 @@
+// This file is part of Hangfire.
+// Copyright © 2013-2014 Sergey Odinokov.
+//
+// Hangfire is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation, either version 3
+// of the License, or any later version.
+//
+// Hangfire is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with Hangfire. If not, see .
+
+using System.Collections.Generic;
+using Hangfire.Raven.Entities;
+
+namespace Hangfire.Raven.JobQueues
+{
+ public interface IPersistentJobQueueMonitoringApi
+ {
+ IEnumerable GetQueues();
+
+ IEnumerable GetEnqueuedJobIds(string queue, int from, int perPage);
+
+ IEnumerable GetFetchedJobIds(string queue, int from, int perPage);
+
+ EnqueuedAndFetchedCount GetEnqueuedAndFetchedCount(string queue);
+ }
+}
diff --git a/src/Hangfire.SqlServer/JobQueues/IPersistentJobQueueProvider.cs b/src/Hangfire.SqlServer/JobQueues/IPersistentJobQueueProvider.cs
new file mode 100644
index 0000000..9eaddab
--- /dev/null
+++ b/src/Hangfire.SqlServer/JobQueues/IPersistentJobQueueProvider.cs
@@ -0,0 +1,24 @@
+// This file is part of Hangfire.
+// Copyright © 2013-2014 Sergey Odinokov.
+//
+// Hangfire is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation, either version 3
+// of the License, or any later version.
+//
+// Hangfire is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with Hangfire. If not, see .
+
+namespace Hangfire.Raven.JobQueues
+{
+ public interface IPersistentJobQueueProvider
+ {
+ IPersistentJobQueue GetJobQueue();
+ IPersistentJobQueueMonitoringApi GetJobQueueMonitoringApi();
+ }
+}
diff --git a/src/Hangfire.SqlServer/JobQueues/PersistentJobQueueProviderCollection.cs b/src/Hangfire.SqlServer/JobQueues/PersistentJobQueueProviderCollection.cs
new file mode 100644
index 0000000..39e1eba
--- /dev/null
+++ b/src/Hangfire.SqlServer/JobQueues/PersistentJobQueueProviderCollection.cs
@@ -0,0 +1,71 @@
+// This file is part of Hangfire.
+// Copyright © 2013-2014 Sergey Odinokov.
+//
+// Hangfire is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation, either version 3
+// of the License, or any later version.
+//
+// Hangfire is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with Hangfire. If not, see .
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
+namespace Hangfire.Raven.JobQueues
+{
+ public class PersistentJobQueueProviderCollection : IEnumerable
+ {
+ private readonly List _providers
+ = new List();
+ private readonly Dictionary _providersByQueue
+ = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ private readonly IPersistentJobQueueProvider _defaultProvider;
+
+ public PersistentJobQueueProviderCollection(IPersistentJobQueueProvider defaultProvider)
+ {
+ defaultProvider.ThrowIfNull("defaultProvider");
+
+ _defaultProvider = defaultProvider;
+
+ _providers.Add(_defaultProvider);
+ }
+
+ public void Add(IPersistentJobQueueProvider provider, IEnumerable queues)
+ {
+ provider.ThrowIfNull("provider");
+ queues.ThrowIfNull("queues");
+
+ _providers.Add(provider);
+
+ foreach (var queue in queues)
+ {
+ _providersByQueue.Add(queue, provider);
+ }
+ }
+
+ public IPersistentJobQueueProvider GetProvider(string queue)
+ {
+ return _providersByQueue.ContainsKey(queue)
+ ? _providersByQueue[queue]
+ : _defaultProvider;
+ }
+
+ public IEnumerator GetEnumerator()
+ {
+ return _providers.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Hangfire.SqlServer/JobQueues/RavenJobQueue.cs b/src/Hangfire.SqlServer/JobQueues/RavenJobQueue.cs
new file mode 100644
index 0000000..6646e83
--- /dev/null
+++ b/src/Hangfire.SqlServer/JobQueues/RavenJobQueue.cs
@@ -0,0 +1,102 @@
+// This file is part of Hangfire.
+// Copyright © 2013-2014 Sergey Odinokov.
+//
+// Hangfire is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation, either version 3
+// of the License, or any later version.
+//
+// Hangfire is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with Hangfire. If not, see .
+
+using System;
+using System.Threading;
+using Hangfire.Annotations;
+using Hangfire.Storage;
+using HangFire.Raven;
+using Raven.Client.Linq;
+using System.Linq;
+using Hangfire.Raven.Entities;
+using Hangfire.Raven.Storage;
+
+namespace Hangfire.Raven.JobQueues
+{
+ public class RavenJobQueue : IPersistentJobQueue
+ {
+ private readonly RavenStorage _storage;
+ private readonly RavenStorageOptions _options;
+
+ private static object _lockMethod = new object();
+
+ public RavenJobQueue([NotNull] RavenStorage storage, RavenStorageOptions options)
+ {
+ storage.ThrowIfNull("storage");
+ options.ThrowIfNull("options");
+
+ _storage = storage;
+ _options = options;
+ }
+
+ [NotNull]
+ public IFetchedJob Dequeue(string[] queues, CancellationToken cancellationToken)
+ {
+ queues.ThrowIfNull("queues");
+
+ if (queues.Length == 0) {
+ throw new ArgumentException("Queue array must be non-empty.", "queues");
+ }
+
+ lock (_lockMethod) {
+
+ JobQueue fetchedJob;
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ do {
+ using (var repository = new Repository()) {
+
+ fetchedJob = repository.Session.Query().FirstOrDefault(t => t.FetchedAt == null
+ && t.Queue.In(queues));
+
+ if (fetchedJob == null) {
+ fetchedJob = repository.Session.Query().FirstOrDefault(t =>
+ t.FetchedAt < DateTime.UtcNow.AddSeconds(_options.InvisibilityTimeout.Negate().TotalSeconds)
+ && t.Queue.In(queues));
+
+ if (fetchedJob == null) {
+ cancellationToken.WaitHandle.WaitOne(_options.QueuePollInterval);
+ cancellationToken.ThrowIfCancellationRequested();
+
+ continue;
+ }
+ }
+
+ fetchedJob.FetchedAt = DateTime.UtcNow;
+
+ repository.Save(fetchedJob);
+ }
+ } while (fetchedJob == null);
+
+ return new RavenFetchedJob(_storage, fetchedJob);
+ }
+ }
+
+ public void Enqueue(string queue, string jobId)
+ {
+ using (var repository = new Repository()) {
+ var jobQueue = new JobQueue
+ {
+ JobId = jobId,
+ Queue = queue
+ };
+
+ repository.Save(jobQueue);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Hangfire.SqlServer/JobQueues/RavenJobQueueMonitoringApi.cs b/src/Hangfire.SqlServer/JobQueues/RavenJobQueueMonitoringApi.cs
new file mode 100644
index 0000000..a4ba004
--- /dev/null
+++ b/src/Hangfire.SqlServer/JobQueues/RavenJobQueueMonitoringApi.cs
@@ -0,0 +1,103 @@
+// This file is part of Hangfire.
+// Copyright © 2013-2014 Sergey Odinokov.
+//
+// Hangfire is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation, either version 3
+// of the License, or any later version.
+//
+// Hangfire is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with Hangfire. If not, see .
+
+using System.Collections.Generic;
+using System.Linq;
+using Hangfire.Raven.Entities;
+using HangFire.Raven;
+using Hangfire.Raven.JobQueues;
+
+namespace Hangfire.Raven.JobQueues
+{
+ internal class RavenJobQueueMonitoringApi : IPersistentJobQueueMonitoringApi
+ {
+ public IEnumerable GetQueues()
+ {
+ using (var repository = new Repository()) {
+ return repository.Session.Query()
+ .Select(x => x.Queue)
+ .Distinct()
+ .ToList();
+ }
+ }
+
+ public IEnumerable GetEnqueuedJobIds(string queue, int @from, int perPage)
+ {
+ int start = @from + 1;
+ int end = from + perPage;
+
+ using (var repository = new Repository()) {
+ var jobs = repository.Session.Query().Where(t => t.Queue == queue && t.FetchedAt == null)
+ .Select((data, i) => new { Index = i + 1, Data = data })
+ .Where(_ => (_.Index >= start) && (_.Index <= end))
+ .Select(x => x.Data)
+ .ToList();
+
+ var results = new List();
+
+ foreach (var item in jobs) {
+ var job = repository.Session.Query().FirstOrDefault(t => t.Id == item.JobId);
+
+ if (job != null && repository.Session.Query().FirstOrDefault(t => t.Id == job.StateId) != null) {
+ results.Add(job);
+ }
+ }
+
+ return results.Select(t => t.Id).ToList();
+ }
+ }
+
+ public IEnumerable GetFetchedJobIds(string queue, int @from, int perPage)
+ {
+ int start = @from + 1;
+ int end = from + perPage;
+
+ using (var repository = new Repository()) {
+ var jobs = repository.Session.Query().Where(t => t.Queue == queue && t.FetchedAt != null)
+ .Select((data, i) => new { Index = i + 1, Data = data })
+ .Where(_ => (_.Index >= start) && (_.Index <= end))
+ .Select(x => x.Data);
+
+ var results = new List();
+
+ foreach (var item in jobs) {
+ var job = repository.Session.Query().FirstOrDefault(t => t.Id == item.JobId);
+
+ if (job != null) {
+ results.Add(job.Id);
+ }
+ }
+
+ return results;
+ }
+ }
+
+ public EnqueuedAndFetchedCount GetEnqueuedAndFetchedCount(string queue)
+ {
+ using (var repository = new Repository()) {
+ int enqueuedCount = repository.Session.Query().Where(t => t.Queue == queue && t.FetchedAt == null).Count();
+
+ int fetchedCount = repository.Session.Query().Where(t => t.Queue == queue && t.FetchedAt != null).Count();
+
+ return new EnqueuedAndFetchedCount
+ {
+ EnqueuedCount = enqueuedCount,
+ FetchedCount = fetchedCount
+ };
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Hangfire.SqlServer/JobQueues/RavenJobQueueProvider.cs b/src/Hangfire.SqlServer/JobQueues/RavenJobQueueProvider.cs
new file mode 100644
index 0000000..55773d6
--- /dev/null
+++ b/src/Hangfire.SqlServer/JobQueues/RavenJobQueueProvider.cs
@@ -0,0 +1,47 @@
+// This file is part of Hangfire.
+// Copyright © 2013-2014 Sergey Odinokov.
+//
+// Hangfire is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation, either version 3
+// of the License, or any later version.
+//
+// Hangfire is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with Hangfire. If not, see .
+
+using System;
+using Hangfire.Annotations;
+using Hangfire.Raven.Storage;
+
+namespace Hangfire.Raven.JobQueues
+{
+ public class RavenJobQueueProvider : IPersistentJobQueueProvider
+ {
+ private readonly IPersistentJobQueue _jobQueue;
+ private readonly IPersistentJobQueueMonitoringApi _monitoringApi;
+
+ public RavenJobQueueProvider([NotNull] RavenStorage storage, [NotNull] RavenStorageOptions options)
+ {
+ storage.ThrowIfNull("storage");
+ options.ThrowIfNull("options");
+
+ _jobQueue = new RavenJobQueue(storage, options);
+ //_monitoringApi = new RavenJobQueueMonitoringApi(storage);
+ }
+
+ public IPersistentJobQueue GetJobQueue()
+ {
+ return _jobQueue;
+ }
+
+ public IPersistentJobQueueMonitoringApi GetJobQueueMonitoringApi()
+ {
+ return _monitoringApi;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Hangfire.SqlServer/Listeners/TakeNewestConflictResolutionListener.cs b/src/Hangfire.SqlServer/Listeners/TakeNewestConflictResolutionListener.cs
new file mode 100644
index 0000000..67845b3
--- /dev/null
+++ b/src/Hangfire.SqlServer/Listeners/TakeNewestConflictResolutionListener.cs
@@ -0,0 +1,24 @@
+using Raven.Abstractions.Data;
+using Raven.Client.Listeners;
+using System.Linq;
+
+namespace Hangfire.Raven.Listeners
+{
+ public class TakeNewestConflictResolutionListener : IDocumentConflictListener
+ {
+ public bool TryResolveConflict(string key, JsonDocument[] conflictedDocs, out JsonDocument resolvedDocument)
+ {
+ var maxDate = conflictedDocs.Max(x => x.LastModified);
+ resolvedDocument = conflictedDocs
+ .FirstOrDefault(x => x.LastModified == maxDate);
+
+ if (resolvedDocument == null)
+ return false;
+
+ resolvedDocument.Metadata.Remove("@id");
+ resolvedDocument.Metadata.Remove("@etag");
+
+ return true;
+ }
+ }
+}
diff --git a/src/Hangfire.SqlServer/ObjectExtensions.cs b/src/Hangfire.SqlServer/ObjectExtensions.cs
new file mode 100644
index 0000000..97c216b
--- /dev/null
+++ b/src/Hangfire.SqlServer/ObjectExtensions.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace Hangfire.Raven
+{
+ public static class ObjectExtensions
+ {
+ public static void ThrowIfNull(this object value, string name)
+ {
+ if (value == null) {
+ throw new ArgumentNullException(name);
+ }
+ }
+ }
+}
diff --git a/src/Hangfire.SqlServer/Properties/AssemblyInfo.cs b/src/Hangfire.SqlServer/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..2ef0d6a
--- /dev/null
+++ b/src/Hangfire.SqlServer/Properties/AssemblyInfo.cs
@@ -0,0 +1,11 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+[assembly: AssemblyTitle("Hangfire.Raven")]
+[assembly: AssemblyDescription("Raven job storage for Hangfire")]
+[assembly: Guid("3d96bf2f-8854-4872-aee3-faf81d121a4d")]
+
+[assembly: InternalsVisibleTo("Hangfire.Raven.Tests")]
+// Allow the generation of mocks for internal types
+[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
diff --git a/src/Hangfire.SqlServer/Raven.Studio.Html5.zip b/src/Hangfire.SqlServer/Raven.Studio.Html5.zip
new file mode 100644
index 0000000..1acf469
Binary files /dev/null and b/src/Hangfire.SqlServer/Raven.Studio.Html5.zip differ
diff --git a/src/Hangfire.SqlServer/RavenConnection.cs b/src/Hangfire.SqlServer/RavenConnection.cs
new file mode 100644
index 0000000..dba07c4
--- /dev/null
+++ b/src/Hangfire.SqlServer/RavenConnection.cs
@@ -0,0 +1,536 @@
+// This file is part of Hangfire.
+// Copyright © 2013-2014 Sergey Odinokov.
+//
+// Hangfire is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation, either version 3
+// of the License, or any later version.
+//
+// Hangfire is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with Hangfire. If not, see .
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using Hangfire.Common;
+using Hangfire.Server;
+using Hangfire.Storage;
+using Raven.Client;
+using Hangfire.Raven.DistributedLock;
+using HangFire.Raven;
+using Hangfire.Raven.Entities;
+using Hangfire.Raven.Storage;
+
+namespace Hangfire.Raven
+{
+ public class RavenConnection : JobStorageConnection
+ {
+ private readonly RavenStorageOptions _options;
+ private readonly RavenStorage _storage;
+
+ public RavenConnection(RavenStorage ravenStorage)
+ {
+ ravenStorage.ThrowIfNull("RavenStorage");
+
+ _storage = ravenStorage;
+ _options = new RavenStorageOptions();
+ }
+
+ public RavenConnection(RavenStorage ravenStorage, RavenStorageOptions options)
+ : this(ravenStorage)
+ {
+ options.ThrowIfNull("options");
+
+ _storage = ravenStorage;
+ _options = options;
+ }
+
+ public override IWriteOnlyTransaction CreateWriteTransaction()
+ {
+ return new RavenWriteOnlyTransaction(_storage);
+ }
+
+ public override IDisposable AcquireDistributedLock(string resource, TimeSpan timeout)
+ {
+ return new RavenDistributedLock(_storage, string.Format("HangFire:{0}", resource), timeout, _options);
+ }
+
+ public static IDocumentStore _documentStore;
+
+ public override IFetchedJob FetchNextJob(string[] queues, CancellationToken cancellationToken)
+ {
+ queues.ThrowIfNull("queues");
+ if (queues.Length == 0) {
+ throw new ArgumentNullException("queues");
+ }
+
+ var providers = queues
+ .Select(queue => _storage.QueueProviders.GetProvider(queue))
+ .Distinct()
+ .ToArray();
+
+ if (providers.Length != 1) {
+ throw new InvalidOperationException(string.Format(
+ "Multiple provider instances registered for queues: {0}. You should choose only one type of persistent queues per server instance.",
+ string.Join(", ", queues)));
+ }
+
+ var persistentQueue = providers[0].GetJobQueue();
+ return persistentQueue.Dequeue(queues, cancellationToken);
+ }
+
+ public override string CreateExpiredJob(
+ Job job,
+ IDictionary parameters,
+ DateTime createdAt,
+ TimeSpan expireIn)
+ {
+ job.ThrowIfNull("job");
+ parameters.ThrowIfNull("parameters");
+
+ var invocationData = InvocationData.Serialize(job);
+
+ var ravenJob = new RavenJob
+ {
+ Id = Guid.NewGuid().ToString(),
+ InvocationData = JobHelper.ToJson(invocationData),
+ Arguments = invocationData.Arguments,
+ CreatedAt = createdAt,
+ ExpireAt = createdAt.Add(expireIn)
+ };
+
+ using (var repository = new Repository()) {
+ repository.Save(ravenJob);
+
+ if (parameters.Count > 0) {
+ foreach (var parameter in parameters) {
+ repository.Save(new JobParameter
+ {
+ JobId = ravenJob.Id,
+ Name = parameter.Key,
+ Value = parameter.Value
+ });
+ }
+ }
+
+ return ravenJob.Id.ToString();
+ }
+ }
+
+ public override JobData GetJobData(string id)
+ {
+ id.ThrowIfNull("id");
+
+ using (var repository = new Repository()) {
+ var jobData = repository.Session.Load(id);
+
+ if (jobData == null) {
+ return null;
+ }
+
+ var invocationData = JobHelper.FromJson(jobData.InvocationData);
+ invocationData.Arguments = jobData.Arguments;
+
+ Job job = null;
+ JobLoadException loadException = null;
+
+ try {
+ job = invocationData.Deserialize();
+ } catch (JobLoadException ex) {
+ loadException = ex;
+ }
+
+ return new JobData
+ {
+ Job = job,
+ State = jobData.StateName,
+ CreatedAt = jobData.CreatedAt,
+ LoadException = loadException
+ };
+ }
+ }
+
+ public override StateData GetStateData(string jobId)
+ {
+ jobId.ThrowIfNull("jobId");
+
+ using (var repository = new Repository()) {
+ RavenJob job = repository.Session.Load(jobId);
+ if (job == null) {
+ return null;
+ }
+
+ State state = repository.Session.Query().FirstOrDefault(t => t.Id == job.StateId);
+ if (state == null) {
+ return null;
+ }
+
+ return new StateData
+ {
+ Name = state.Name,
+ Reason = state.Reason,
+ Data = JobHelper.FromJson>(state.Data)
+ };
+ }
+ }
+
+ public override void SetJobParameter(string id, string name, string value)
+ {
+ id.ThrowIfNull("id");
+ name.ThrowIfNull("name");
+
+ using (var repository = new Repository()) {
+ var results = repository.Session.Query().Where(t => t.JobId == id && t.Name == name).ToList();
+
+ results.ForEach(t => {
+ t.Value = value;
+ repository.Save(t);
+ });
+ }
+ }
+
+ public override string GetJobParameter(string id, string name)
+ {
+ id.ThrowIfNull("id");
+ name.ThrowIfNull("name");
+
+ using (var repository = new Repository()) {
+ var jobParameter = repository.Session.Query().FirstOrDefault(t => t.JobId == id && t.Name == name);
+
+ return jobParameter != null ? jobParameter.Value : null;
+ }
+ }
+
+ public override HashSet GetAllItemsFromSet(string key)
+ {
+ key.ThrowIfNull("key");
+
+ using (var repository = new Repository()) {
+ var results = repository.Session.Query().Where(t => t.Key == key).Select(t => t.Value).ToList();
+
+ return new HashSet(results);
+ }
+ }
+
+ public override string GetFirstByLowestScoreFromSet(string key, double fromScore, double toScore)
+ {
+ key.ThrowIfNull("key");
+
+ if (toScore < fromScore) {
+ throw new ArgumentException("The `toScore` value must be higher or equal to the `fromScore` value.");
+ }
+
+ using (var repository = new Repository()) {
+ var set = repository.Session.Query()
+ .Where(t => t.Key == key)
+ .Where(t => t.Score >= fromScore)
+ .Where(t => t.Score <= toScore)
+ .OrderBy(t => t.Score)
+ .FirstOrDefault();
+
+
+ return set != null ? set.Value : null;
+ }
+ }
+
+ public override void SetRangeInHash(string key, IEnumerable> keyValuePairs)
+ {
+ key.ThrowIfNull("key");
+ keyValuePairs.ThrowIfNull("keyValuePairs");
+
+ using (var repository = new Repository()) {
+ foreach (var keyValuePair in keyValuePairs) {
+ var results = repository.Session.Query().Where(t => t.Key == key && t.Field == keyValuePair.Key).ToList();
+
+ results.ForEach(t =>
+ {
+ t.Value = keyValuePair.Value;
+ repository.Save(t);
+ });
+ }
+ }
+ }
+
+ public override Dictionary GetAllEntriesFromHash(string key)
+ {
+ key.ThrowIfNull("key");
+
+ using (var session = new Repository().Session) {
+ var result = session.Query().Where(t => t.Key == key)
+ .ToDictionary(x => x.Field, x => x.Value);
+
+ return result.Count != 0 ? result : null;
+ }
+ }
+
+ public override void AnnounceServer(string serverId, ServerContext context)
+ {
+ serverId.ThrowIfNull("serverId");
+ context.ThrowIfNull("context");
+
+ var data = new ServerData
+ {
+ WorkerCount = context.WorkerCount,
+ Queues = context.Queues,
+ StartedAt = DateTime.UtcNow,
+ };
+
+ using (var repository = new Repository()) {
+ var servers = repository.Session.Query()
+ .Where(t => t.Id == serverId)
+ .ToList();
+
+ var queues = servers.SelectMany(t => JobHelper.FromJson(t.Data).Queues).ToList();
+ queues.AddRange(data.Queues.Select(t => t).ToList());
+ data.Queues = queues.ToArray();
+
+ data.WorkerCount += servers.Select(t => JobHelper.FromJson(t.Data).WorkerCount).Sum(t => t);
+
+ foreach (var item in servers) {
+ repository.Delete(item);
+ }
+
+ var server = new RavenServer
+ {
+ Data = JobHelper.ToJson(data),
+ Id = serverId,
+ LastHeartbeat = DateTime.UtcNow
+ };
+
+ repository.Save(server);
+ }
+ }
+
+ public override void RemoveServer(string serverId)
+ {
+ serverId.ThrowIfNull("serverId");
+
+ using (var repository = new Repository()) {
+ var servers = repository.Session.Query()
+ .Where(t => t.Id == serverId)
+ .ToList();
+
+ foreach (var item in servers) {
+ repository.Delete(item);
+ }
+ }
+ }
+
+ public override void Heartbeat(string serverId)
+ {
+ serverId.ThrowIfNull("serverId");
+
+ using (var repository = new Repository()) {
+ var server = repository.Session.Load(serverId);
+
+ if (server == null) {
+ server = new RavenServer
+ {
+ Id = serverId
+ };
+ }
+
+ server.LastHeartbeat = DateTime.UtcNow;
+
+ repository.Save(server);
+ }
+ }
+
+ public override int RemoveTimedOutServers(TimeSpan timeOut)
+ {
+ if (timeOut.Duration() != timeOut) {
+ throw new ArgumentException("The `timeOut` value must be positive.", "timeOut");
+ }
+
+ using (var repository = new Repository()) {
+ var heartBeatCutOff = DateTime.UtcNow.Add(timeOut.Negate());
+
+ var results = repository.Session.Query()
+ .Where(t => t.LastHeartbeat < heartBeatCutOff)
+ .ToList();
+
+ foreach (var item in results) {
+ repository.Delete(item);
+ }
+
+ return results.Count;
+ }
+ }
+
+ public override long GetSetCount(string key)
+ {
+ key.ThrowIfNull("key");
+
+ using (var repository = new Repository()) {
+ var count = repository.Session.Query()
+ .Where(t => t.Key == key)
+ .Count();
+
+ return count;
+ }
+ }
+
+ public override List GetRangeFromSet(string key, int startingFrom, int endingAt)
+ {
+ key.ThrowIfNull("key");
+
+ using (var repository = new Repository()) {
+ var values = repository.Session.Query()
+ .Where(t => t.Key == key)
+ .Skip(startingFrom - 1)
+ .Take(endingAt - startingFrom + 1)
+ .Select(t => t.Value)
+ .ToList();
+
+ return values;
+ }
+ }
+
+ public override TimeSpan GetSetTtl(string key)
+ {
+ key.ThrowIfNull("key");
+
+ using (var repository = new Repository()) {
+ var expireAt = repository.Session.Query()
+ .Where(t => t.Key == key)
+ .Select(t => t.ExpireAt)
+ .ToArray();
+
+ if (expireAt.Any() == false) {
+ return TimeSpan.FromSeconds(-1);
+ }
+
+ return expireAt.Where(t => t.HasValue).Min(t => t.Value) - DateTime.UtcNow;
+ }
+ }
+
+ public override long GetCounter(string key)
+ {
+ key.ThrowIfNull("key");
+
+ using (var repository = new Repository()) {
+ var values = repository.Session.Query()
+ .Where(t => t.Key == key)
+ .Select(t => t.Value)
+ .ToArray();
+
+ return values.Any() ? values.Sum() : 0;
+ }
+ }
+
+ public override long GetHashCount(string key)
+ {
+ key.ThrowIfNull("key");
+
+ using (var repository = new Repository()) {
+ var count = repository.Session.Query()
+ .Where(t => t.Key == key)
+ .Count();
+
+ return count;
+ }
+ }
+
+ public override TimeSpan GetHashTtl(string key)
+ {
+ key.ThrowIfNull("key");
+
+ using (var repository = new Repository()) {
+ var hashes = repository.Session.Query()
+ .Where(t => t.Key == key)
+ .ToList();
+
+ DateTime? result = hashes.Any() ? hashes.Min(x => x.ExpireAt) : null;
+
+ if (!result.HasValue) {
+ return TimeSpan.FromSeconds(-1);
+ }
+
+ return result.Value - DateTime.UtcNow;
+ }
+ }
+
+ public override string GetValueFromHash(string key, string name)
+ {
+ key.ThrowIfNull("key");
+ name.ThrowIfNull("name");
+
+ using (var repository = new Repository()) {
+ var result = repository.Session.Query()
+ .Where(t => t.Key == key)
+ .Where(t => t.Field == name)
+ .FirstOrDefault();
+
+ return result != null ? result.Value : null;
+ }
+ }
+
+ public override long GetListCount(string key)
+ {
+ key.ThrowIfNull("key");
+
+ using (var repository = new Repository()) {
+ var count = repository.Session.Query()
+ .Where(t => t.Key == key)
+ .Count();
+
+ return count;
+ }
+ }
+
+ public override TimeSpan GetListTtl(string key)
+ {
+ key.ThrowIfNull("key");
+
+ using (var repository = new Repository()) {
+ var results = repository.Session.Query()
+ .Where(t => t.Key == key)
+ .ToList();
+
+ DateTime? ttl = results.Any() ? results.Min(t => t.ExpireAt) : (DateTime?)null;
+
+ if (!ttl.HasValue) {
+ return TimeSpan.FromSeconds(-1);
+ }
+
+ return ttl.Value - DateTime.UtcNow;
+ }
+ }
+
+ public override List GetRangeFromList(string key, int startingFrom, int endingAt)
+ {
+ key.ThrowIfNull("key");
+
+ using (var repository = new Repository()) {
+ var results = repository.Session.Query()
+ .Where(t => t.Key == key)
+ .Select(t => t.Value)
+ .Skip(startingFrom - 1)
+ .Take(endingAt - startingFrom + 1)
+ .ToList();
+
+ return results;
+ }
+ }
+
+ public override List GetAllItemsFromList(string key)
+ {
+ key.ThrowIfNull("key");
+
+ using (var repository = new Repository()) {
+ var results = repository.Session.Query()
+ .Where(t => t.Key == key)
+ .Select(t => t.Value)
+ .ToList();
+
+ return results;
+ }
+ }
+ }
+}
diff --git a/src/Hangfire.SqlServer/RavenWriteOnlyTransaction.cs b/src/Hangfire.SqlServer/RavenWriteOnlyTransaction.cs
new file mode 100644
index 0000000..2ffced0
--- /dev/null
+++ b/src/Hangfire.SqlServer/RavenWriteOnlyTransaction.cs
@@ -0,0 +1,373 @@
+// This file is part of Hangfire.
+// Copyright © 2013-2014 Sergey Odinokov.
+//
+// Hangfire is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation, either version 3
+// of the License, or any later version.
+//
+// Hangfire is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with Hangfire. If not, see .
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Hangfire.Annotations;
+using Hangfire.Common;
+using Hangfire.States;
+using Hangfire.Storage;
+using HangFire.Raven;
+using Hangfire.Raven.Entities;
+using Hangfire.Raven.Storage;
+
+namespace Hangfire.Raven
+{
+ internal class RavenWriteOnlyTransaction : JobStorageTransaction
+ {
+ private readonly Queue> _commandQueue
+ = new Queue>();
+
+ private readonly SortedSet _lockedResources = new SortedSet();
+ private readonly RavenStorage _storage;
+ public RavenWriteOnlyTransaction([NotNull] RavenStorage storage)
+ {
+ storage.ThrowIfNull("storage");
+
+ _storage = storage;
+ }
+
+ public override void Commit()
+ {
+ foreach (var command in _commandQueue) {
+ command(new RavenConnection(_storage));
+ }
+ }
+
+ public override void ExpireJob(string jobId, TimeSpan expireIn)
+ {
+ using (var repository = new Repository()) {
+ var result = repository.Session.Load(jobId);
+
+ result.ExpireAt = DateTime.UtcNow.Add(expireIn);
+
+ repository.Save(result);
+ }
+ }
+
+ public override void PersistJob(string jobId)
+ {
+ using (var repository = new Repository()) {
+ var result = repository.Session.Load(jobId);
+
+ result.ExpireAt = null;
+
+ repository.Save(result);
+ }
+ }
+
+ public override void SetJobState(string jobId, IState state)
+ {
+ using (var repository = new Repository()) {
+ var jobState = repository.Session.Query().FirstOrDefault(t => t.JobId == jobId);
+
+ if (jobState == null) {
+ jobState = new State
+ {
+ CreatedAt = DateTime.UtcNow,
+ JobId = jobId
+ };
+ }
+
+ jobState.Reason = state.Reason;
+ jobState.Name = state.Name;
+ jobState.Data = JobHelper.ToJson(state.SerializeData());
+
+ repository.Save(jobState);
+
+ var result = repository.Session.Load(jobId);
+
+ result.StateId = jobState.Id;
+ result.StateName = state.Name;
+ result.StateData = JobHelper.ToJson(state.SerializeData());
+ result.StateReason = state.Reason;
+
+ repository.Save(result);
+ }
+ }
+
+ public override void AddJobState(string jobId, IState state)
+ {
+ using (var repository = new Repository()) {
+ var jobState = new State
+ {
+ Id = Guid.NewGuid().ToString(),
+ JobId = jobId,
+ Name = state.Name,
+ Reason = state.Reason,
+ CreatedAt = DateTime.UtcNow,
+ Data = JobHelper.ToJson(state.SerializeData())
+ };
+
+ repository.Save(jobState);
+ }
+ }
+
+ public override void AddToQueue(string queue, string jobId)
+ {
+ var provider = _storage.QueueProviders.GetProvider(queue);
+ var persistentQueue = provider.GetJobQueue();
+
+ persistentQueue.Enqueue(queue, jobId);
+ }
+
+ public override void IncrementCounter(string key)
+ {
+ using (var repository = new Repository()) {
+ var counter = repository.Session.Query().FirstOrDefault(t => t.Key == key);
+
+ if (counter == null) {
+ counter = new Counter
+ {
+ Id = Guid.NewGuid().ToString(),
+ Key = key,
+ Value = 0
+ };
+ }
+
+ counter.Value += 1;
+
+ repository.Save(counter);
+ }
+ }
+
+ public override void IncrementCounter(string key, TimeSpan expireIn)
+ {
+ using (var repository = new Repository()) {
+ var counter = repository.Session.Query().FirstOrDefault(t => t.Key == key);
+
+ if (counter == null) {
+ counter = new Counter
+ {
+ Id = Guid.NewGuid().ToString(),
+ ExpireAt = DateTime.UtcNow.Add(expireIn),
+ Key = key,
+ Value = 0
+ };
+ }
+
+ counter.Value += 1;
+
+ repository.Save(counter);
+ }
+ }
+
+ public override void DecrementCounter(string key)
+ {
+ using (var repository = new Repository()) {
+ var counter = repository.Session.Query().FirstOrDefault(t => t.Key == key);
+
+ if (counter == null) {
+ counter = new Counter
+ {
+ Id = Guid.NewGuid().ToString(),
+ Key = key,
+ Value = 0
+ };
+ }
+
+ counter.Value -= 1;
+
+ repository.Save(counter);
+ }
+ }
+
+ public override void DecrementCounter(string key, TimeSpan expireIn)
+ {
+ using (var repository = new Repository()) {
+ var counter = repository.Session.Query().FirstOrDefault(t => t.Key == key);
+
+ if (counter == null) {
+ counter = new Counter
+ {
+ Id = Guid.NewGuid().ToString(),
+ ExpireAt = DateTime.UtcNow.Add(expireIn),
+ Key = key,
+ Value = 0
+ };
+ }
+
+ counter.Value -= 1;
+
+ repository.Save(counter);
+ }
+ }
+
+ public override void AddToSet(string key, string value)
+ {
+ AddToSet(key, value, 0.0);
+ }
+
+ public override void AddToSet(string key, string value, double score)
+ {
+ using (var repository = new Repository()) {
+ var set = repository.Session.Query().FirstOrDefault(t => t.Key == key && t.Value == value);
+
+ if (set == null) {
+ set = new RavenSet
+ {
+ Id = Guid.NewGuid().ToString(),
+ Key = key,
+ Value = value
+ };
+ }
+
+ set.Score = score;
+
+ repository.Save(set);
+ }
+ }
+
+ public override void RemoveFromSet(string key, string value)
+ {
+ using (var repository = new Repository()) {
+ var set = repository.Session.Query().FirstOrDefault(t => t.Key == key && t.Value == value);
+
+ if (set == null) {
+ return;
+ }
+
+ repository.Delete(set);
+ }
+ }
+
+ public override void InsertToList(string key, string value)
+ {
+ using (var repository = new Repository()) {
+ var list = repository.Session.Query().FirstOrDefault(t => t.Key == key && t.Value == value);
+
+ if (list == null) {
+ list = new RavenList
+ {
+ Id = Guid.NewGuid().ToString(),
+ Key = key
+ };
+ }
+
+ list.Value = value;
+
+ repository.Save(list);
+ }
+ }
+
+ public override void RemoveFromList(string key, string value)
+ {
+ using (var repository = new Repository()) {
+ var list = repository.Session.Query().FirstOrDefault(t => t.Key == key && t.Value == value);
+
+ if (list == null) {
+ return;
+ }
+
+ repository.Delete(list);
+ }
+ }
+
+ public override void TrimList(string key, int keepStartingFrom, int keepEndingAt)
+ {
+ using (var repository = new Repository()) {
+ var list = repository.Session.Query().Where(t => t.Key == key).ToList();
+
+ if (!list.Any()) {
+ return;
+ }
+
+ for (var i = 0; i < list.Count; ++i) {
+ if (i >= keepStartingFrom && i <= keepEndingAt) {
+ continue;
+ }
+
+ repository.Delete(list[i]);
+ }
+ }
+ }
+
+ public override void SetRangeInHash(string key, IEnumerable> keyValuePairs)
+ {
+ key.ThrowIfNull("key");
+ keyValuePairs.ThrowIfNull("keyValuePairs");
+
+ using (var repository = new Repository()) {
+ foreach (var keyValuePair in keyValuePairs) {
+ var pair = keyValuePair;
+
+ var results = repository.Session.Query().Where(t => t.Key == key && t.Field == pair.Key).ToList();
+
+ if (!results.Any()) {
+ results.Add(new RavenHash
+ {
+ Key = key,
+ Field = pair.Key,
+ });
+ }
+
+ foreach (var item in results) {
+ item.Value = pair.Value;
+
+ repository.Save(item);
+ }
+ }
+ }
+ }
+
+ public override void RemoveHash(string key)
+ {
+ key.ThrowIfNull("key");
+
+ using (var repository = new Repository()) {
+ var result = repository.Session.Query().Where(t => t.Key == key).ToList();
+
+ foreach (var item in result) {
+ repository.Session.Delete(item);
+ }
+ }
+ }
+
+ public override void RemoveSet(string key)
+ {
+ key.ThrowIfNull("key");
+
+ using (var repository = new Repository()) {
+ var result = repository.Session.Query().Where(t => t.Key == key).ToList();
+
+ foreach (var item in result) {
+ repository.Session.Delete(item);
+ }
+ }
+ }
+
+ public override void ExpireHash(string key, TimeSpan expireIn)
+ {
+ key.ThrowIfNull("key");
+
+ using (var repository = new Repository()) {
+ var result = repository.Session.Query().Where(t => t.Key == key).ToList();
+
+ foreach (var item in result) {
+ item.ExpireAt = DateTime.UtcNow.Add(expireIn);
+
+ repository.Save(item);
+ }
+ }
+ }
+
+ internal void QueueCommand(Action action)
+ {
+ _commandQueue.Enqueue(action);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Hangfire.SqlServer/Repository.cs b/src/Hangfire.SqlServer/Repository.cs
new file mode 100644
index 0000000..7acd9d8
--- /dev/null
+++ b/src/Hangfire.SqlServer/Repository.cs
@@ -0,0 +1,358 @@
+using Raven.Client;
+using System;
+using Dewey.Net.Types;
+using Raven.Client.Document;
+using Raven.Client.Embedded;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+using Dewey.Net.Generic;
+using Raven.Abstractions.Data;
+using System.Collections.Concurrent;
+using Raven.Client.Linq;
+using System.Linq;
+using System.Linq.Expressions;
+using Hangfire.Raven.Entities.Identity;
+using Hangfire.Raven.Extensions;
+using Hangfire.Raven.Listeners;
+
+namespace HangFire.Raven
+{
+ public class Repository : IDisposable
+ {
+ public static string ConnectionUrl { get; set; }
+ public static string DefaultDatabase { get; set; }
+
+ private static IDocumentStore _documentStore;
+ private IDocumentSession _session;
+ private IAsyncDocumentSession _asyncSession;
+ private string _database;
+
+ public static bool Embedded { get; set; }
+
+ public string Database => _database ?? DefaultDatabase;
+
+ public void Destroy()
+ {
+ if (!DocumentStore.DatabaseExists(Database)) {
+ return;
+ }
+
+ DocumentStore.DatabaseCommands.GlobalAdmin.DeleteDatabase(Database, hardDelete: true);
+ }
+
+ public void Create()
+ {
+ if (DocumentStore.DatabaseExists(Database)) {
+ return;
+ }
+
+ DocumentStore.DatabaseCommands.GlobalAdmin.EnsureDatabaseExists(Database);
+ }
+
+ public IDocumentStore DocumentStore
+ {
+ get
+ {
+ if (_documentStore == null) {
+ if (Embedded) {
+ _documentStore = new EmbeddableDocumentStore
+ {
+ RunInMemory = true
+ };
+
+ ((EmbeddableDocumentStore)_documentStore).Configuration.Storage.Voron.AllowOn32Bits = true;
+ } else {
+ _documentStore = new DocumentStore
+ {
+ Url = ConnectionUrl
+ };
+ }
+
+ _documentStore.Conventions.DefaultQueryingConsistency = ConsistencyOptions.AlwaysWaitForNonStaleResultsAsOfLastWrite;
+ _documentStore.Listeners.RegisterListener(new TakeNewestConflictResolutionListener());
+ _documentStore.Conventions.DefaultQueryingConsistency = ConsistencyOptions.AlwaysWaitForNonStaleResultsAsOfLastWrite;
+
+ _documentStore.Initialize();
+ }
+
+ return _documentStore;
+ }
+ }
+
+ public IDocumentSession Session => _session ?? (_session = DocumentStore.OpenSession(Database));
+
+ public IAsyncDocumentSession AsyncSession => _asyncSession ?? (_asyncSession = DocumentStore.OpenAsyncSession(Database));
+
+ #region constructor
+
+ public Repository()
+ {
+ _database = DefaultDatabase;
+ }
+
+ public Repository(string database)
+ {
+ if (database.IsEmpty()) {
+ _database = DefaultDatabase;
+ } else {
+ _database = database;
+ }
+ }
+
+ #endregion
+
+ #region Bulk Insert
+
+ private BulkInsertOptions GetBulkInsertOptions(bool overwrite = true) => new BulkInsertOptions
+ {
+ BatchSize = 100,
+ WriteTimeoutMilliseconds = 360000,
+ OverwriteExisting = overwrite
+ };
+
+ public void BulkInsert(ConcurrentBag entities, bool overwrite = true) where T : BaseEntity
+ {
+ if (_database.IsEmpty()) {
+ _database = DefaultDatabase;
+ }
+
+ using (var bulkInsert = DocumentStore.BulkInsert(Database, GetBulkInsertOptions(overwrite))) {
+ foreach (var item in entities) {
+ if (item == null) {
+ continue;
+ }
+
+ if (item.Id.IsEmpty()) {
+ item.Id = Guid.NewGuid().ToString();
+ }
+
+ bulkInsert.Store(item);
+ }
+ }
+ }
+
+ public void BulkInsert(List entities, bool overwrite = true) where T : BaseEntity
+ {
+ var concurrentBag = new ConcurrentBag();
+
+ entities.ForEach(t => concurrentBag.Add(t));
+
+ BulkInsert(concurrentBag, overwrite);
+ }
+
+ #endregion
+
+ #region Find
+
+ public T FindById(string id) where T : BaseEntity
+ {
+ if (id == null) {
+ throw new ArgumentException("Id must be provided.", nameof(id));
+ }
+
+ return Session.Load(id);
+ }
+
+ public async Task FindByIdAsync(string id) where T : BaseEntity
+ {
+ if (id == null) {
+ throw new ArgumentException("Id must be provided.", nameof(id));
+ }
+
+ return await AsyncSession.LoadAsync(id);
+ }
+
+ #endregion
+
+ #region Save
+
+ public void Save(T entity) where T : BaseEntity
+ {
+ if (entity == null) {
+ throw new ArgumentException("Entity must be provided.", nameof(entity));
+ }
+
+ if (entity.Id.IsEmpty()) {
+ entity.Id = Guid.NewGuid().ToString();
+ }
+
+ Session.Store(entity);
+ Session.SaveChanges();
+ }
+
+ public async Task SaveAsync(T entity) where T : BaseEntity
+ {
+ if (entity == null) {
+ throw new ArgumentException("Entity must be provided.", nameof(entity));
+ }
+
+ if (entity.Id.IsEmpty()) {
+ entity.Id = Guid.NewGuid().ToString();
+ }
+
+ await AsyncSession.StoreAsync(entity);
+ await AsyncSession.SaveChangesAsync();
+ }
+
+ public async Task SaveNoValidateAsync(T entity) where T : BaseEntity
+ {
+ if (entity == null) {
+ throw new ArgumentException("Entity must be provided.", nameof(entity));
+ }
+
+ if (entity.Id.IsEmpty()) {
+ entity.Id = Guid.NewGuid().ToString();
+ }
+
+ await AsyncSession.StoreAsync(entity);
+ await AsyncSession.SaveChangesAsync();
+ }
+
+ public void SaveAll(List entities) where T : BaseEntity
+ {
+ if (entities.IsEmpty()) {
+ throw new ArgumentException("Data must be provided.", nameof(entities));
+ }
+
+ foreach (var item in entities) {
+ Save(item);
+ }
+ }
+
+ public async Task SaveAllAsync(List entities) where T : BaseEntity
+ {
+ if (entities.IsEmpty()) {
+ throw new ArgumentException("Data must be provided.", nameof(entities));
+ }
+
+ foreach (var item in entities) {
+ await SaveAsync(item);
+ }
+ }
+
+ #endregion
+
+ #region Delete
+
+ public void Delete(string id)
+ {
+ if (id == null) {
+ throw new ArgumentException("Id must be provided.", nameof(id));
+ }
+
+ Session.Delete(id);
+
+ Session.SaveChanges();
+ }
+
+ public async Task DeleteAsync(string id)
+ {
+ if (id == null) {
+ throw new ArgumentException("Id must be provided.", nameof(id));
+ }
+
+ AsyncSession.Delete(id);
+
+ await AsyncSession.SaveChangesAsync();
+ }
+
+ public void Delete(T entity) where T : BaseEntity
+ {
+ if (entity == null) {
+ throw new ArgumentException("Entity must be provided.", nameof(entity));
+ }
+
+ Session.Delete(entity.Id);
+
+ Session.SaveChanges();
+ }
+
+ public async Task DeleteAsync(T entity) where T : BaseEntity
+ {
+ if (entity == null) {
+ throw new ArgumentException("Entity must be provided.", nameof(entity));
+ }
+
+ AsyncSession.Delete(entity.Id);
+
+ await AsyncSession.SaveChangesAsync();
+ }
+
+ #endregion
+
+ #region Query
+
+ public IRavenQueryable Query() where T : BaseEntity
+ {
+ return Session.Query();
+ }
+
+ public IRavenQueryable QueryAsync() where T : BaseEntity
+ {
+ return AsyncSession.Query();
+ }
+
+ #endregion
+
+ #region Index
+
+ public IRavenQueryable Index(string indexName, bool isMapReduce = false) where T : BaseEntity
+ {
+ return Session.Query(indexName, isMapReduce);
+ }
+
+ public IRavenQueryable IndexAsync(string indexName, bool isMapReduce = false) where T : BaseEntity
+ {
+ return AsyncSession.Query(indexName, isMapReduce);
+ }
+
+ #endregion
+
+ #region Count
+
+ public int Count() where T : BaseEntity
+ {
+ return Session.Query().Count();
+ }
+
+ public async Task CountAsync() where T : BaseEntity
+ {
+ return await AsyncSession.Query().CountAsync();
+ }
+
+ public int Count(Expression> predicate) where T : BaseEntity
+ {
+ return Session.Query().Where(predicate).Count();
+ }
+
+ public async Task CountAsync(Expression> predicate) where T : BaseEntity
+ {
+ return await AsyncSession.Query().Where(predicate).CountAsync();
+ }
+
+ #endregion
+
+ #region IDisposable
+
+ private bool _disposed = false;
+
+ protected void Dispose(bool disposing)
+ {
+ if (!_disposed) {
+ if (disposing) {
+ _session = null;
+ _asyncSession = null;
+ }
+
+ _disposed = true;
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/src/Hangfire.SqlServer/SqlServerMonitoringApi.cs b/src/Hangfire.SqlServer/SqlServerMonitoringApi.cs
new file mode 100644
index 0000000..c7df715
--- /dev/null
+++ b/src/Hangfire.SqlServer/SqlServerMonitoringApi.cs
@@ -0,0 +1,562 @@
+//// This file is part of Hangfire.
+//// Copyright © 2013-2014 Sergey Odinokov.
+////
+//// Hangfire is free software: you can redistribute it and/or modify
+//// it under the terms of the GNU Lesser General Public License as
+//// published by the Free Software Foundation, either version 3
+//// of the License, or any later version.
+////
+//// Hangfire is distributed in the hope that it will be useful,
+//// but WITHOUT ANY WARRANTY; without even the implied warranty of
+//// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+//// GNU Lesser General Public License for more details.
+////
+//// You should have received a copy of the GNU Lesser General Public
+//// License along with Hangfire. If not, see .
+
+//using System;
+//using System.Collections.Generic;
+//using System.Data.SqlClient;
+//using System.Linq;
+//using System.Transactions;
+//using Dapper;
+//using Hangfire.Annotations;
+//using Hangfire.Common;
+//using Hangfire.SqlServer.Entities;
+//using Hangfire.States;
+//using Hangfire.Storage;
+//using Hangfire.Storage.Monitoring;
+
+//namespace Hangfire.SqlServer
+//{
+// internal class SqlServerMonitoringApi : IMonitoringApi
+// {
+// private readonly RavenStorage _storage;
+// private readonly int? _jobListLimit;
+
+// public SqlServerMonitoringApi([NotNull] RavenStorage storage, int? jobListLimit)
+// {
+// if (storage == null) throw new ArgumentNullException("storage");
+
+// _storage = storage;
+// _jobListLimit = jobListLimit;
+// }
+
+// public long ScheduledCount()
+// {
+// return UseConnection(connection =>
+// GetNumberOfJobsByStateName(connection, ScheduledState.StateName));
+// }
+
+// public long EnqueuedCount(string queue)
+// {
+// var queueApi = GetQueueApi(queue);
+// var counters = queueApi.GetEnqueuedAndFetchedCount(queue);
+
+// return counters.EnqueuedCount ?? 0;
+// }
+
+// public long FetchedCount(string queue)
+// {
+// var queueApi = GetQueueApi(queue);
+// var counters = queueApi.GetEnqueuedAndFetchedCount(queue);
+
+// return counters.FetchedCount ?? 0;
+// }
+
+// public long FailedCount()
+// {
+// return UseConnection(connection =>
+// GetNumberOfJobsByStateName(connection, FailedState.StateName));
+// }
+
+// public long ProcessingCount()
+// {
+// return UseConnection(connection =>
+// GetNumberOfJobsByStateName(connection, ProcessingState.StateName));
+// }
+
+// public JobList ProcessingJobs(int @from, int count)
+// {
+// return UseConnection(connection => GetJobs(
+// connection,
+// from, count,
+// ProcessingState.StateName,
+// (sqlJob, job, stateData) => new ProcessingJobDto
+// {
+// Job = job,
+// ServerId = stateData.ContainsKey("ServerId") ? stateData["ServerId"] : stateData["ServerName"],
+// StartedAt = JobHelper.DeserializeDateTime(stateData["StartedAt"]),
+// }));
+// }
+
+// public JobList ScheduledJobs(int @from, int count)
+// {
+// return UseConnection(connection => GetJobs(
+// connection,
+// from, count,
+// ScheduledState.StateName,
+// (sqlJob, job, stateData) => new ScheduledJobDto
+// {
+// Job = job,
+// EnqueueAt = JobHelper.DeserializeDateTime(stateData["EnqueueAt"]),
+// ScheduledAt = JobHelper.DeserializeDateTime(stateData["ScheduledAt"])
+// }));
+// }
+
+// public IDictionary SucceededByDatesCount()
+// {
+// return UseConnection(connection =>
+// GetTimelineStats(connection, "succeeded"));
+// }
+
+// public IDictionary FailedByDatesCount()
+// {
+// return UseConnection(connection =>
+// GetTimelineStats(connection, "failed"));
+// }
+
+// public IList Servers()
+// {
+// return UseConnection>(connection =>
+// {
+// var servers = connection.Query(
+// string.Format(@"select * from [{0}].Server", _storage.GetSchemaName()))
+// .ToList();
+
+// var result = new List();
+
+// foreach (var server in servers)
+// {
+// var data = JobHelper.FromJson(server.Data);
+// result.Add(new ServerDto
+// {
+// Name = server.Id,
+// Heartbeat = server.LastHeartbeat,
+// Queues = data.Queues,
+// StartedAt = data.StartedAt.HasValue ? data.StartedAt.Value : DateTime.MinValue,
+// WorkersCount = data.WorkerCount
+// });
+// }
+
+// return result;
+// });
+// }
+
+// public JobList FailedJobs(int @from, int count)
+// {
+// return UseConnection(connection => GetJobs(
+// connection,
+// from,
+// count,
+// FailedState.StateName,
+// (sqlJob, job, stateData) => new FailedJobDto
+// {
+// Job = job,
+// Reason = sqlJob.StateReason,
+// ExceptionDetails = stateData["ExceptionDetails"],
+// ExceptionMessage = stateData["ExceptionMessage"],
+// ExceptionType = stateData["ExceptionType"],
+// FailedAt = JobHelper.DeserializeNullableDateTime(stateData["FailedAt"])
+// }));
+// }
+
+// public JobList SucceededJobs(int @from, int count)
+// {
+// return UseConnection(connection => GetJobs(
+// connection,
+// from,
+// count,
+// SucceededState.StateName,
+// (sqlJob, job, stateData) => new SucceededJobDto
+// {
+// Job = job,
+// Result = stateData.ContainsKey("Result") ? stateData["Result"] : null,
+// TotalDuration = stateData.ContainsKey("PerformanceDuration") && stateData.ContainsKey("Latency")
+// ? (long?)long.Parse(stateData["PerformanceDuration"]) + (long?)long.Parse(stateData["Latency"])
+// : null,
+// SucceededAt = JobHelper.DeserializeNullableDateTime(stateData["SucceededAt"])
+// }));
+// }
+
+// public JobList DeletedJobs(int @from, int count)
+// {
+// return UseConnection(connection => GetJobs(
+// connection,
+// from,
+// count,
+// DeletedState.StateName,
+// (sqlJob, job, stateData) => new DeletedJobDto
+// {
+// Job = job,
+// DeletedAt = JobHelper.DeserializeNullableDateTime(stateData["DeletedAt"])
+// }));
+// }
+
+// public IList Queues()
+// {
+// var tuples = _storage.QueueProviders
+// .Select(x => x.GetJobQueueMonitoringApi())
+// .SelectMany(x => x.GetQueues(), (monitoring, queue) => new { Monitoring = monitoring, Queue = queue })
+// .OrderBy(x => x.Queue)
+// .ToArray();
+
+// var result = new List(tuples.Length);
+
+// foreach (var tuple in tuples)
+// {
+// var enqueuedJobIds = tuple.Monitoring.GetEnqueuedJobIds(tuple.Queue, 0, 5);
+// var counters = tuple.Monitoring.GetEnqueuedAndFetchedCount(tuple.Queue);
+
+// var firstJobs = UseConnection(connection => EnqueuedJobs(connection, enqueuedJobIds));
+
+// result.Add(new QueueWithTopEnqueuedJobsDto
+// {
+// Name = tuple.Queue,
+// Length = counters.EnqueuedCount ?? 0,
+// Fetched = counters.FetchedCount,
+// FirstJobs = firstJobs
+// });
+// }
+
+// return result;
+// }
+
+// public JobList EnqueuedJobs(string queue, int @from, int perPage)
+// {
+// var queueApi = GetQueueApi(queue);
+// var enqueuedJobIds = queueApi.GetEnqueuedJobIds(queue, from, perPage);
+
+// return UseConnection(connection => EnqueuedJobs(connection, enqueuedJobIds));
+// }
+
+// public JobList FetchedJobs(string queue, int @from, int perPage)
+// {
+// var queueApi = GetQueueApi(queue);
+// var fetchedJobIds = queueApi.GetFetchedJobIds(queue, from, perPage);
+
+// return UseConnection(connection => FetchedJobs(connection, fetchedJobIds));
+// }
+
+// public IDictionary HourlySucceededJobs()
+// {
+// return UseConnection(connection =>
+// GetHourlyTimelineStats(connection, "succeeded"));
+// }
+
+// public IDictionary HourlyFailedJobs()
+// {
+// return UseConnection(connection =>
+// GetHourlyTimelineStats(connection, "failed"));
+// }
+
+// public JobDetailsDto JobDetails(string jobId)
+// {
+// return UseConnection(connection =>
+// {
+
+// string sql = string.Format(@"
+//select * from [{0}].Job where Id = @id
+//select * from [{0}].JobParameter where JobId = @id
+//select * from [{0}].State where JobId = @id order by Id desc", _storage.GetSchemaName());
+
+// using (var multi = connection.QueryMultiple(sql, new { id = jobId }))
+// {
+// var job = multi.Read().SingleOrDefault();
+// if (job == null) return null;
+
+// var parameters = multi.Read().ToDictionary(x => x.Name, x => x.Value);
+// var history =
+// multi.Read()
+// .ToList()
+// .Select(x => new StateHistoryDto
+// {
+// StateName = x.Name,
+// CreatedAt = x.CreatedAt,
+// Reason = x.Reason,
+// Data = new Dictionary(
+// JobHelper.FromJson>(x.Data),
+// StringComparer.OrdinalIgnoreCase),
+// })
+// .ToList();
+
+// return new JobDetailsDto
+// {
+// CreatedAt = job.CreatedAt,
+// ExpireAt = job.ExpireAt,
+// Job = DeserializeJob(job.InvocationData, job.Arguments),
+// History = history,
+// Properties = parameters
+// };
+// }
+// });
+// }
+
+// public long SucceededListCount()
+// {
+// return UseConnection(connection =>
+// GetNumberOfJobsByStateName(connection, SucceededState.StateName));
+// }
+
+// public long DeletedListCount()
+// {
+// return UseConnection(connection =>
+// GetNumberOfJobsByStateName(connection, DeletedState.StateName));
+// }
+
+// public StatisticsDto GetStatistics()
+// {
+// string sql = string.Format(@"
+//select count(Id) from [{0}].Job where StateName = N'Enqueued';
+//select count(Id) from [{0}].Job where StateName = N'Failed';
+//select count(Id) from [{0}].Job where StateName = N'Processing';
+//select count(Id) from [{0}].Job where StateName = N'Scheduled';
+//select count(Id) from [{0}].Server;
+//select sum(s.[Value]) from (
+// select sum([Value]) as [Value] from [{0}].Counter where [Key] = N'stats:succeeded'
+// union all
+// select [Value] from [{0}].AggregatedCounter where [Key] = N'stats:succeeded'
+//) as s;
+//select sum(s.[Value]) from (
+// select sum([Value]) as [Value] from [{0}].Counter where [Key] = N'stats:deleted'
+// union all
+// select [Value] from [{0}].AggregatedCounter where [Key] = N'stats:deleted'
+//) as s;
+//select count(*) from [{0}].[Set] where [Key] = N'recurring-jobs';
+//", _storage.GetSchemaName());
+
+// var statistics = UseConnection(connection =>
+// {
+// var stats = new StatisticsDto();
+// using (var multi = connection.QueryMultiple(sql))
+// {
+// stats.Enqueued = multi.Read().Single();
+// stats.Failed = multi.Read().Single();
+// stats.Processing = multi.Read().Single();
+// stats.Scheduled = multi.Read().Single();
+
+// stats.Servers = multi.Read().Single();
+
+// stats.Succeeded = multi.Read().SingleOrDefault() ?? 0;
+// stats.Deleted = multi.Read().SingleOrDefault() ?? 0;
+
+// stats.Recurring = multi.Read().Single();
+// }
+// return stats;
+// });
+
+// statistics.Queues = _storage.QueueProviders
+// .SelectMany(x => x.GetJobQueueMonitoringApi().GetQueues())
+// .Count();
+
+// return statistics;
+// }
+
+// private Dictionary GetHourlyTimelineStats(
+// SqlConnection connection,
+// string type)
+// {
+// var endDate = DateTime.UtcNow;
+// var dates = new List();
+// for (var i = 0; i < 24; i++)
+// {
+// dates.Add(endDate);
+// endDate = endDate.AddHours(-1);
+// }
+
+// var keyMaps = dates.ToDictionary(x => String.Format("stats:{0}:{1}", type, x.ToString("yyyy-MM-dd-HH")), x => x);
+
+// return GetTimelineStats(connection, keyMaps);
+// }
+
+// private Dictionary GetTimelineStats(
+// SqlConnection connection,
+// string type)
+// {
+// var endDate = DateTime.UtcNow.Date;
+// var dates = new List();
+// for (var i = 0; i < 7; i++)
+// {
+// dates.Add(endDate);
+// endDate = endDate.AddDays(-1);
+// }
+
+// var keyMaps = dates.ToDictionary(x => String.Format("stats:{0}:{1}", type, x.ToString("yyyy-MM-dd")), x => x);
+
+// return GetTimelineStats(connection, keyMaps);
+// }
+
+// private Dictionary GetTimelineStats(SqlConnection connection,
+// IDictionary keyMaps)
+// {
+// string sqlQuery = string.Format(@"
+//select [Key], [Value] as [Count] from [{0}].AggregatedCounter
+//where [Key] in @keys", _storage.GetSchemaName());
+
+// var valuesMap = connection.Query(
+// sqlQuery,
+// new { keys = keyMaps.Keys })
+// .ToDictionary(x => (string)x.Key, x => (long)x.Count);
+
+// foreach (var key in keyMaps.Keys)
+// {
+// if (!valuesMap.ContainsKey(key)) valuesMap.Add(key, 0);
+// }
+
+// var result = new Dictionary();
+// for (var i = 0; i < keyMaps.Count; i++)
+// {
+// var value = valuesMap[keyMaps.ElementAt(i).Key];
+// result.Add(keyMaps.ElementAt(i).Value, value);
+// }
+
+// return result;
+// }
+
+// private IPersistentJobQueueMonitoringApi GetQueueApi(string queueName)
+// {
+// var provider = _storage.QueueProviders.GetProvider(queueName);
+// var monitoringApi = provider.GetJobQueueMonitoringApi();
+
+// return monitoringApi;
+// }
+
+// private T UseConnection(Func action)
+// {
+// return _storage.UseTransaction(action, IsolationLevel.ReadUncommitted);
+// }
+
+// private JobList EnqueuedJobs(
+// SqlConnection connection,
+// IEnumerable jobIds)
+// {
+// string enqueuedJobsSql = string.Format(@"
+//select j.*, s.Reason as StateReason, s.Data as StateData
+//from [{0}].Job j
+//left join [{0}].State s on s.Id = j.StateId
+//where j.Id in @jobIds", _storage.GetSchemaName());
+
+// var jobs = connection.Query(
+// enqueuedJobsSql,
+// new { jobIds = jobIds })
+// .ToList();
+
+// return DeserializeJobs(
+// jobs,
+// (sqlJob, job, stateData) => new EnqueuedJobDto
+// {
+// Job = job,
+// State = sqlJob.StateName,
+// EnqueuedAt = sqlJob.StateName == EnqueuedState.StateName
+// ? JobHelper.DeserializeNullableDateTime(stateData["EnqueuedAt"])
+// : null
+// });
+// }
+
+// private long GetNumberOfJobsByStateName(SqlConnection connection, string stateName)
+// {
+// var sqlQuery = _jobListLimit.HasValue
+// ? string.Format(@"select count(j.Id) from (select top (@limit) Id from [{0}].Job where StateName = @state) as j", _storage.GetSchemaName())
+// : string.Format(@"select count(Id) from [{0}].Job where StateName = @state", _storage.GetSchemaName());
+
+// var count = connection.Query(
+// sqlQuery,
+// new { state = stateName, limit = _jobListLimit })
+// .Single();
+
+// return count;
+// }
+
+// private static Job DeserializeJob(string invocationData, string arguments)
+// {
+// var data = JobHelper.FromJson(invocationData);
+// data.Arguments = arguments;
+
+// try
+// {
+// return data.Deserialize();
+// }
+// catch (JobLoadException)
+// {
+// return null;
+// }
+// }
+
+// private JobList GetJobs(
+// SqlConnection connection,
+// int from,
+// int count,
+// string stateName,
+// Func, TDto> selector)
+// {
+// string jobsSql = string.Format(@"
+//select * from (
+// select j.*, s.Reason as StateReason, s.Data as StateData, row_number() over (order by j.Id desc) as row_num
+// from [{0}].Job j with (forceseek)
+// left join [{0}].State s on j.StateId = s.Id
+// where j.StateName = @stateName
+//) as j where j.row_num between @start and @end
+//", _storage.GetSchemaName());
+
+// var jobs = connection.Query(
+// jobsSql,
+// new { stateName = stateName, start = @from + 1, end = @from + count })
+// .ToList();
+
+// return DeserializeJobs(jobs, selector);
+// }
+
+// private static JobList DeserializeJobs(
+// ICollection jobs,
+// Func, TDto> selector)
+// {
+// var result = new List>(jobs.Count);
+
+// foreach (var job in jobs)
+// {
+// var deserializedData = JobHelper.FromJson>(job.StateData);
+// var stateData = deserializedData != null
+// ? new Dictionary(deserializedData, StringComparer.OrdinalIgnoreCase)
+// : null;
+
+// var dto = selector(job, DeserializeJob(job.InvocationData, job.Arguments), stateData);
+
+// result.Add(new KeyValuePair(
+// job.Id.ToString(), dto));
+// }
+
+// return new JobList(result);
+// }
+
+// private JobList FetchedJobs(
+// SqlConnection connection,
+// IEnumerable jobIds)
+// {
+// string fetchedJobsSql = string.Format(@"
+//select j.*, s.Reason as StateReason, s.Data as StateData
+//from [{0}].Job j
+//left join [{0}].State s on s.Id = j.StateId
+//where j.Id in @jobIds", _storage.GetSchemaName());
+
+// var jobs = connection.Query(
+// fetchedJobsSql,
+// new { jobIds = jobIds })
+// .ToList();
+
+// var result = new List>(jobs.Count);
+
+// foreach (var job in jobs)
+// {
+// result.Add(new KeyValuePair(
+// job.Id.ToString(),
+// new FetchedJobDto
+// {
+// Job = DeserializeJob(job.InvocationData, job.Arguments),
+// State = job.StateName,
+// }));
+// }
+
+// return new JobList(result);
+// }
+// }
+//}
diff --git a/src/Hangfire.SqlServer/StateHandlers/DeletedStateHandler.cs b/src/Hangfire.SqlServer/StateHandlers/DeletedStateHandler.cs
new file mode 100644
index 0000000..d71897c
--- /dev/null
+++ b/src/Hangfire.SqlServer/StateHandlers/DeletedStateHandler.cs
@@ -0,0 +1,24 @@
+using Hangfire.States;
+using Hangfire.Storage;
+
+namespace Hangfire.Raven.StateHandlers
+{
+ public class DeletedStateHandler : IStateHandler
+ {
+ public void Apply(ApplyStateContext context, IWriteOnlyTransaction transaction)
+ {
+ transaction.InsertToList("deleted", context.JobId);
+ transaction.TrimList("deleted", 0, 99);
+ }
+
+ public void Unapply(ApplyStateContext context, IWriteOnlyTransaction transaction)
+ {
+ transaction.RemoveFromList("deleted", context.JobId);
+ }
+
+ public string StateName
+ {
+ get { return DeletedState.StateName; }
+ }
+ }
+}
diff --git a/src/Hangfire.SqlServer/StateHandlers/FailedStateHandler.cs b/src/Hangfire.SqlServer/StateHandlers/FailedStateHandler.cs
new file mode 100644
index 0000000..0cca9f4
--- /dev/null
+++ b/src/Hangfire.SqlServer/StateHandlers/FailedStateHandler.cs
@@ -0,0 +1,25 @@
+using Hangfire.Common;
+using Hangfire.States;
+using Hangfire.Storage;
+using System;
+
+namespace Hangfire.Raven.StateHandlers
+{
+ public class FailedStateHandler : IStateHandler
+ {
+ public void Apply(ApplyStateContext context, IWriteOnlyTransaction transaction)
+ {
+ transaction.AddToSet("failed", context.JobId, JobHelper.ToTimestamp(DateTime.UtcNow));
+ }
+
+ public void Unapply(ApplyStateContext context, IWriteOnlyTransaction transaction)
+ {
+ transaction.RemoveFromSet("failed", context.JobId);
+ }
+
+ public string StateName
+ {
+ get { return FailedState.StateName; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Hangfire.SqlServer/StateHandlers/ProcessingStateHandler.cs b/src/Hangfire.SqlServer/StateHandlers/ProcessingStateHandler.cs
new file mode 100644
index 0000000..a647de4
--- /dev/null
+++ b/src/Hangfire.SqlServer/StateHandlers/ProcessingStateHandler.cs
@@ -0,0 +1,25 @@
+using Hangfire.Common;
+using Hangfire.States;
+using Hangfire.Storage;
+using System;
+
+namespace Hangfire.Raven.StateHandlers
+{
+ public class ProcessingStateHandler : IStateHandler
+ {
+ public void Apply(ApplyStateContext context, IWriteOnlyTransaction transaction)
+ {
+ transaction.AddToSet("processing", context.JobId, JobHelper.ToTimestamp(DateTime.UtcNow));
+ }
+
+ public void Unapply(ApplyStateContext context, IWriteOnlyTransaction transaction)
+ {
+ transaction.RemoveFromSet("processing", context.JobId);
+ }
+
+ public string StateName
+ {
+ get { return ProcessingState.StateName; }
+ }
+ }
+}
diff --git a/src/Hangfire.SqlServer/StateHandlers/SucceededStateHandler.cs b/src/Hangfire.SqlServer/StateHandlers/SucceededStateHandler.cs
new file mode 100644
index 0000000..9f721e8
--- /dev/null
+++ b/src/Hangfire.SqlServer/StateHandlers/SucceededStateHandler.cs
@@ -0,0 +1,24 @@
+using Hangfire.States;
+using Hangfire.Storage;
+
+namespace Hangfire.Raven.StateHandlers
+{
+ public class SucceededStateHandler : IStateHandler
+ {
+ public void Apply(ApplyStateContext context, IWriteOnlyTransaction transaction)
+ {
+ transaction.InsertToList("succeeded", context.JobId);
+ transaction.TrimList("succeeded", 0, 99);
+ }
+
+ public void Unapply(ApplyStateContext context, IWriteOnlyTransaction transaction)
+ {
+ transaction.RemoveFromList("succeeded", context.JobId);
+ }
+
+ public string StateName
+ {
+ get { return SucceededState.StateName; }
+ }
+ }
+}
diff --git a/src/Hangfire.SqlServer/Storage/RavenStorage.cs b/src/Hangfire.SqlServer/Storage/RavenStorage.cs
new file mode 100644
index 0000000..513e870
--- /dev/null
+++ b/src/Hangfire.SqlServer/Storage/RavenStorage.cs
@@ -0,0 +1,87 @@
+// This file is part of Hangfire.
+// Copyright © 2013-2014 Sergey Odinokov.
+//
+// Hangfire is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation, either version 3
+// of the License, or any later version.
+//
+// Hangfire is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with Hangfire. If not, see .
+
+using System;
+using System.Collections.Generic;
+using Hangfire.Logging;
+using Hangfire.Server;
+using Hangfire.Storage;
+using Raven.Client.Document;
+using Hangfire.Raven.JobQueues;
+
+namespace Hangfire.Raven.Storage
+{
+ public class RavenStorage : JobStorage
+ {
+ private readonly RavenStorageOptions _options;
+
+ public RavenStorage()
+ : this(new RavenStorageOptions())
+ {
+ }
+
+ ///
+ /// Initializes RavenStorage from the provided SqlServerStorageOptions and either the provided connection
+ /// string or the connection string with provided name pulled from the application config file.
+ ///
+ ///
+ ///
+ /// argument is null.
+ /// argument is null.
+ /// argument is neither
+ /// a valid Raven connection string nor the name of a connection string in the application
+ /// config file.
+ public RavenStorage(RavenStorageOptions options)
+ {
+ options.ThrowIfNull("options");
+
+ _options = options;
+
+ InitializeQueueProviders();
+ }
+
+ public virtual PersistentJobQueueProviderCollection QueueProviders { get; private set; }
+
+ public override IMonitoringApi GetMonitoringApi()
+ {
+ //return new SqlServerMonitoringApi(this, _options.DashboardJobListLimit);
+
+ return null;
+ }
+
+ public override IStorageConnection GetConnection()
+ {
+ return new RavenConnection(this);
+ }
+
+ public override IEnumerable GetComponents()
+ {
+ yield return new ExpirationManager(_options.JobExpirationCheckInterval);
+ }
+
+ public override void WriteOptionsToLog(ILog logger)
+ {
+ logger.Info("Using the following options for Raven job storage:");
+ logger.InfoFormat("Queue poll interval: {0}.", _options.QueuePollInterval);
+ }
+
+ private void InitializeQueueProviders()
+ {
+ var defaultQueueProvider = new RavenJobQueueProvider(this, _options);
+ QueueProviders = new PersistentJobQueueProviderCollection(defaultQueueProvider);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Hangfire.SqlServer/Storage/RavenStorageExtensions.cs b/src/Hangfire.SqlServer/Storage/RavenStorageExtensions.cs
new file mode 100644
index 0000000..7f67897
--- /dev/null
+++ b/src/Hangfire.SqlServer/Storage/RavenStorageExtensions.cs
@@ -0,0 +1,87 @@
+// This file is part of Hangfire.
+// Copyright © 2015 Sergey Odinokov.
+//
+// Hangfire is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation, either version 3
+// of the License, or any later version.
+//
+// Hangfire is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with Hangfire. If not, see .
+
+using System;
+using Hangfire;
+using Hangfire.Raven.Storage;
+using Hangfire.Raven;
+
+// ReSharper disable once CheckNamespace
+namespace HangFire.Raven.Storage
+{
+ public static class SqlServerStorageExtensions
+ {
+ public static IGlobalConfiguration UseRavenStorage(this IGlobalConfiguration configuration, string connectionUrl, string database)
+ {
+ configuration.ThrowIfNull("configuration");
+ connectionUrl.ThrowIfNull("connectionUrl");
+ database.ThrowIfNull("database");
+
+ if (!connectionUrl.StartsWith("http")) {
+ throw new ArgumentException("Connection Url must begin with http or https!");
+ }
+
+ Repository.ConnectionUrl = connectionUrl;
+ Repository.DefaultDatabase = database;
+
+ var storage = new RavenStorage();
+
+ return configuration.UseStorage(storage);
+ }
+
+ public static IGlobalConfiguration UseRavenStorage(this IGlobalConfiguration configuration, string connectionUrl, string database, RavenStorageOptions options)
+ {
+ configuration.ThrowIfNull("configuration");
+ connectionUrl.ThrowIfNull("connectionUrl");
+ database.ThrowIfNull("database");
+ options.ThrowIfNull("options");
+
+ if (!connectionUrl.StartsWith("http")) {
+ throw new ArgumentException("Connection Url must begin with http or https!");
+ }
+
+ Repository.ConnectionUrl = connectionUrl;
+ Repository.DefaultDatabase = database;
+
+ var storage = new RavenStorage(options);
+
+ return configuration.UseStorage(storage);
+ }
+
+ public static IGlobalConfiguration UseEmbeddedRavenStorage(this IGlobalConfiguration configuration)
+ {
+ configuration.ThrowIfNull("configuration");
+
+ Repository.Embedded = true;
+
+ var storage = new RavenStorage();
+
+ return configuration.UseStorage(storage);
+ }
+
+ public static IGlobalConfiguration UseEmbeddedRavenStorage(this IGlobalConfiguration configuration, RavenStorageOptions options)
+ {
+ configuration.ThrowIfNull("configuration");
+ options.ThrowIfNull("options");
+
+ Repository.Embedded = true;
+
+ var storage = new RavenStorage(options);
+
+ return configuration.UseStorage(storage);
+ }
+ }
+}
diff --git a/src/Hangfire.SqlServer/Storage/RavenStorageOptions.cs b/src/Hangfire.SqlServer/Storage/RavenStorageOptions.cs
new file mode 100644
index 0000000..be12324
--- /dev/null
+++ b/src/Hangfire.SqlServer/Storage/RavenStorageOptions.cs
@@ -0,0 +1,80 @@
+// This file is part of Hangfire.
+// Copyright © 2013-2014 Sergey Odinokov.
+//
+// Hangfire is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation, either version 3
+// of the License, or any later version.
+//
+// Hangfire is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with Hangfire. If not, see .
+
+using System;
+
+namespace Hangfire.Raven.Storage
+{
+ public class RavenStorageOptions
+ {
+ private readonly string _clientId = null;
+
+ private TimeSpan _queuePollInterval;
+
+ public RavenStorageOptions()
+ {
+ QueuePollInterval = TimeSpan.FromSeconds(15);
+ InvisibilityTimeout = TimeSpan.FromMinutes(30);
+ JobExpirationCheckInterval = TimeSpan.FromHours(1);
+ CountersAggregateInterval = TimeSpan.FromMinutes(5);
+ PrepareSchemaIfNecessary = true;
+ DashboardJobListLimit = 50000;
+ TransactionTimeout = TimeSpan.FromMinutes(1);
+ DistributedLockLifetime = TimeSpan.FromSeconds(30);
+
+ _clientId = Guid.NewGuid().ToString().Replace("-", String.Empty);
+ }
+
+ public TimeSpan QueuePollInterval
+ {
+ get { return _queuePollInterval; }
+ set
+ {
+ var message = string.Format(
+ "The QueuePollInterval property value should be positive. Given: {0}.",
+ value);
+
+ if (value == TimeSpan.Zero)
+ {
+ throw new ArgumentException(message, "value");
+ }
+ if (value != value.Duration())
+ {
+ throw new ArgumentException(message, "value");
+ }
+
+ _queuePollInterval = value;
+ }
+ }
+
+ [Obsolete("Does not make sense anymore. Background jobs re-queued instantly even after ungraceful shutdown now. Will be removed in 2.0.0.")]
+ public TimeSpan InvisibilityTimeout { get; set; }
+
+ public bool PrepareSchemaIfNecessary { get; set; }
+
+ public TimeSpan JobExpirationCheckInterval { get; set; }
+ public TimeSpan CountersAggregateInterval { get; set; }
+
+ public int? DashboardJobListLimit { get; set; }
+ public TimeSpan TransactionTimeout { get; set; }
+ public TimeSpan DistributedLockLifetime { get; set; }
+
+ public string ClientId
+ {
+ get { return _clientId; }
+ }
+ }
+}
diff --git a/src/Hangfire.SqlServer/app.config b/src/Hangfire.SqlServer/app.config
new file mode 100644
index 0000000..195db1f
--- /dev/null
+++ b/src/Hangfire.SqlServer/app.config
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Hangfire.SqlServer/packages.config b/src/Hangfire.SqlServer/packages.config
new file mode 100644
index 0000000..f611865
--- /dev/null
+++ b/src/Hangfire.SqlServer/packages.config
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/SharedAssemblyInfo.cs b/src/SharedAssemblyInfo.cs
new file mode 100644
index 0000000..490beeb
--- /dev/null
+++ b/src/SharedAssemblyInfo.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+[assembly: AssemblyProduct("Hangfire")]
+[assembly: AssemblyCompany("Sergey Odinokov")]
+[assembly: AssemblyCopyright("Copyright © 2013-2015 Sergey Odinokov")]
+[assembly: AssemblyCulture("")]
+
+[assembly: ComVisible(false)]
+[assembly: CLSCompliant(false)]
+
+// Don't edit manually! Use `build.bat version` command instead!
+[assembly: AssemblyVersion("1.5.0")]
diff --git a/tests/Hangfire.Mongo.Tests/ExpirationManagerFacts.cs b/tests/Hangfire.Mongo.Tests/ExpirationManagerFacts.cs
new file mode 100644
index 0000000..9d1c26b
--- /dev/null
+++ b/tests/Hangfire.Mongo.Tests/ExpirationManagerFacts.cs
@@ -0,0 +1,230 @@
+using Hangfire.Mongo.Database;
+using Hangfire.Mongo.Dto;
+using Hangfire.Mongo.MongoUtils;
+using Hangfire.Mongo.Tests.Utils;
+using MongoDB.Bson;
+using MongoDB.Bson.Serialization;
+using MongoDB.Driver;
+using MongoDB.Driver.Builders;
+using System;
+using System.Threading;
+using Xunit;
+
+namespace Hangfire.Mongo.Tests
+{
+ public class ExpirationManagerFacts
+ {
+ private readonly MongoStorage _storage;
+
+ private readonly CancellationToken _token;
+
+ public ExpirationManagerFacts()
+ {
+ _storage = new MongoStorage(ConnectionUtils.GetConnectionString(), ConnectionUtils.GetDatabaseName());
+ _token = new CancellationToken(true);
+ }
+
+ [Fact]
+ public void Ctor_ThrowsAnException_WhenStorageIsNull()
+ {
+ Assert.Throws(() => new ExpirationManager(null));
+ }
+
+ [Fact, CleanDatabase]
+ public void Execute_RemovesOutdatedRecords()
+ {
+ using (var connection = ConnectionUtils.CreateConnection())
+ {
+ var entryId = CreateExpirationEntry(connection, connection.GetServerTimeUtc().AddMonths(-1));
+ var manager = CreateManager();
+
+ manager.Execute(_token);
+
+ Assert.True(IsEntryExpired(connection, entryId));
+ }
+ }
+
+ [Fact, CleanDatabase]
+ public void Execute_DoesNotRemoveEntries_WithNoExpirationTimeSet()
+ {
+ using (var connection = ConnectionUtils.CreateConnection())
+ {
+ var entryId = CreateExpirationEntry(connection, null);
+ var manager = CreateManager();
+
+ manager.Execute(_token);
+
+ Assert.False(IsEntryExpired(connection, entryId));
+ }
+ }
+
+ [Fact, CleanDatabase]
+ public void Execute_DoesNotRemoveEntries_WithFreshExpirationTime()
+ {
+ using (var connection = ConnectionUtils.CreateConnection())
+ {
+ var entryId = CreateExpirationEntry(connection, DateTime.Now.AddMonths(1));
+ var manager = CreateManager();
+
+ manager.Execute(_token);
+
+ Assert.False(IsEntryExpired(connection, entryId));
+ }
+ }
+
+ [Fact, CleanDatabase]
+ public void Execute_Processes_CounterTable()
+ {
+ using (var connection = ConnectionUtils.CreateConnection())
+ {
+ // Arrange
+ connection.Counter.Insert(new CounterDto
+ {
+ Id = ObjectId.GenerateNewId(),
+ Key = "key",
+ Value = 1,
+ ExpireAt = connection.GetServerTimeUtc().AddMonths(-1)
+ });
+
+ var manager = CreateManager();
+
+ // Act
+ manager.Execute(_token);
+
+ // Assert
+ var count = connection.Counter.Count();
+ Assert.Equal(0, count);
+ }
+ }
+
+ [Fact, CleanDatabase]
+ public void Execute_Processes_JobTable()
+ {
+ using (var connection = ConnectionUtils.CreateConnection())
+ {
+ // Arrange
+ connection.Job.Insert(new JobDto
+ {
+ Id = 1,
+ InvocationData = "",
+ Arguments = "",
+ CreatedAt = connection.GetServerTimeUtc(),
+ ExpireAt = connection.GetServerTimeUtc().AddMonths(-1),
+ });
+
+ var manager = CreateManager();
+
+ // Act
+ manager.Execute(_token);
+
+ // Assert
+ var count = connection.Job.Count();
+ Assert.Equal(0, count);
+ }
+ }
+
+ [Fact, CleanDatabase]
+ public void Execute_Processes_ListTable()
+ {
+ using (var connection = ConnectionUtils.CreateConnection())
+ {
+ // Arrange
+ connection.List.Insert(new ListDto
+ {
+ Id = ObjectId.GenerateNewId(),
+ Key = "key",
+ ExpireAt = connection.GetServerTimeUtc().AddMonths(-1)
+ });
+
+ var manager = CreateManager();
+
+ // Act
+ manager.Execute(_token);
+
+ // Assert
+ var count = connection.List.Count();
+ Assert.Equal(0, count);
+ }
+ }
+
+ [Fact, CleanDatabase]
+ public void Execute_Processes_SetTable()
+ {
+ using (var connection = ConnectionUtils.CreateConnection())
+ {
+ // Arrange
+ connection.Set.Insert(new SetDto
+ {
+ Id = ObjectId.GenerateNewId(),
+ Key = "key",
+ Score = 0,
+ Value = "",
+ ExpireAt = connection.GetServerTimeUtc().AddMonths(-1)
+ });
+
+ var manager = CreateManager();
+
+ // Act
+ manager.Execute(_token);
+
+ // Assert
+ var count = connection.Set.Count();
+ Assert.Equal(0, count);
+ }
+ }
+
+ [Fact, CleanDatabase]
+ public void Execute_Processes_HashTable()
+ {
+ using (var connection = ConnectionUtils.CreateConnection())
+ {
+ // Arrange
+ connection.Hash.Insert(new HashDto
+ {
+ Id = ObjectId.GenerateNewId(),
+ Key = "key",
+ Field = "field",
+ Value = "",
+ ExpireAt = connection.GetServerTimeUtc().AddMonths(-1)
+ });
+
+ var manager = CreateManager();
+
+ // Act
+ manager.Execute(_token);
+
+ // Assert
+ var count = connection.Hash.Count();
+ Assert.Equal(0, count);
+ }
+ }
+
+
+ private static ObjectId CreateExpirationEntry(HangfireDbContext connection, DateTime? expireAt)
+ {
+ var counter = new CounterDto
+ {
+ Id = ObjectId.GenerateNewId(),
+ Key = "key",
+ Value = 1,
+ ExpireAt = expireAt
+ };
+ connection.Counter.Insert(counter);
+
+ var id = counter.Id;
+
+ return id;
+ }
+
+ private static bool IsEntryExpired(HangfireDbContext connection, ObjectId entryId)
+ {
+ var count = connection.Counter.Count(Query.EQ(_ => _.Id, entryId));
+ return count == 0;
+ }
+
+ private ExpirationManager CreateManager()
+ {
+ return new ExpirationManager(_storage);
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Hangfire.Mongo.Tests/Hangfire.Mongo.Tests.csproj b/tests/Hangfire.Mongo.Tests/Hangfire.Mongo.Tests.csproj
new file mode 100644
index 0000000..7d8e17e
--- /dev/null
+++ b/tests/Hangfire.Mongo.Tests/Hangfire.Mongo.Tests.csproj
@@ -0,0 +1,103 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {237F15A4-26DB-4FBD-86AF-81E87AD5762A}
+ Library
+ Properties
+ Hangfire.Mongo.Tests
+ Hangfire.Mongo.Tests
+ v4.5.1
+ 512
+ ..\..\
+ true
+
+
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+
+ False
+ ..\..\packages\Hangfire.Core.1.3.0\lib\net45\Hangfire.Core.dll
+
+
+ False
+ ..\..\packages\mongocsharpdriver.1.9.2\lib\net35\MongoDB.Bson.dll
+
+
+ False
+ ..\..\packages\mongocsharpdriver.1.9.2\lib\net35\MongoDB.Driver.dll
+
+
+ ..\..\packages\Moq.4.2.1409.1722\lib\net40\Moq.dll
+
+
+ ..\..\packages\Newtonsoft.Json.5.0.1\lib\net45\Newtonsoft.Json.dll
+
+
+ False
+ ..\..\packages\Owin.1.0\lib\net40\Owin.dll
+
+
+
+
+
+
+
+
+
+ ..\..\packages\xunit.1.9.2\lib\net20\xunit.dll
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Designer
+
+
+
+
+ {1fe9c86e-db7a-4c1b-b73a-42b993d68688}
+ Hangfire.Mongo
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/Hangfire.Mongo.Tests/MongoConnectionFacts.cs b/tests/Hangfire.Mongo.Tests/MongoConnectionFacts.cs
new file mode 100644
index 0000000..ebac6c5
--- /dev/null
+++ b/tests/Hangfire.Mongo.Tests/MongoConnectionFacts.cs
@@ -0,0 +1,869 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using Hangfire.Common;
+using Hangfire.Mongo.Database;
+using Hangfire.Mongo.Dto;
+using Hangfire.Mongo.MongoUtils;
+using Hangfire.Mongo.PersistentJobQueue;
+using Hangfire.Mongo.Tests.Utils;
+using Hangfire.Server;
+using Hangfire.Storage;
+using MongoDB.Bson;
+using MongoDB.Driver.Builders;
+using Moq;
+using Xunit;
+
+namespace Hangfire.Mongo.Tests
+{
+ public class MongoConnectionFacts
+ {
+ private readonly Mock _queue;
+ private readonly Mock _provider;
+ private readonly PersistentJobQueueProviderCollection _providers;
+
+ public MongoConnectionFacts()
+ {
+ _queue = new Mock();
+
+ _provider = new Mock();
+ _provider.Setup(x => x.GetJobQueue(It.IsNotNull())).Returns(_queue.Object);
+
+ _providers = new PersistentJobQueueProviderCollection(_provider.Object);
+ }
+
+ [Fact]
+ public void Ctor_ThrowsAnException_WhenConnectionIsNull()
+ {
+ var exception = Assert.Throws(
+ () => new MongoConnection(null, _providers));
+
+ Assert.Equal("database", exception.ParamName);
+ }
+
+ [Fact, CleanDatabase]
+ public void Ctor_ThrowsAnException_WhenProvidersCollectionIsNull()
+ {
+ var exception = Assert.Throws(
+ () => new MongoConnection(ConnectionUtils.CreateConnection(), null));
+
+ Assert.Equal("queueProviders", exception.ParamName);
+ }
+
+
+ [Fact, CleanDatabase]
+ public void FetchNextJob_DelegatesItsExecution_ToTheQueue()
+ {
+ UseConnection((database, connection) =>
+ {
+ var token = new CancellationToken();
+ var queues = new[] { "default" };
+
+ connection.FetchNextJob(queues, token);
+
+ _queue.Verify(x => x.Dequeue(queues, token));
+ });
+ }
+
+ [Fact, CleanDatabase]
+ public void FetchNextJob_Throws_IfMultipleProvidersResolved()
+ {
+ UseConnection((database, connection) =>
+ {
+ var token = new CancellationToken();
+ var anotherProvider = new Mock();
+ _providers.Add(anotherProvider.Object, new[] { "critical" });
+
+ Assert.Throws(
+ () => connection.FetchNextJob(new[] { "critical", "default" }, token));
+ });
+ }
+
+ [Fact, CleanDatabase]
+ public void CreateWriteTransaction_ReturnsNonNullInstance()
+ {
+ UseConnection((database, connection) =>
+ {
+ var transaction = connection.CreateWriteTransaction();
+ Assert.NotNull(transaction);
+ });
+ }
+
+ [Fact, CleanDatabase]
+ public void AcquireLock_ReturnsNonNullInstance()
+ {
+ UseConnection((database, connection) =>
+ {
+ var @lock = connection.AcquireDistributedLock("1", TimeSpan.FromSeconds(1));
+ Assert.NotNull(@lock);
+ });
+ }
+
+ [Fact, CleanDatabase]
+ public void CreateExpiredJob_ThrowsAnException_WhenJobIsNull()
+ {
+ UseConnection((database, connection) =>
+ {
+ var exception = Assert.Throws(
+ () => connection.CreateExpiredJob(
+ null,
+ new Dictionary(),
+ database.GetServerTimeUtc(),
+ TimeSpan.Zero));
+
+ Assert.Equal("job", exception.ParamName);
+ });
+ }
+
+ [Fact, CleanDatabase]
+ public void CreateExpiredJob_ThrowsAnException_WhenParametersCollectionIsNull()
+ {
+ UseConnection((database, connection) =>
+ {
+ var exception = Assert.Throws(
+ () => connection.CreateExpiredJob(
+ Job.FromExpression(() => SampleMethod("hello")),
+ null,
+ database.GetServerTimeUtc(),
+ TimeSpan.Zero));
+
+ Assert.Equal("parameters", exception.ParamName);
+ });
+ }
+
+ [Fact, CleanDatabase]
+ public void CreateExpiredJob_CreatesAJobInTheStorage_AndSetsItsParameters()
+ {
+ UseConnection((database, connection) =>
+ {
+ var createdAt = new DateTime(2012, 12, 12, 0, 0, 0, 0, DateTimeKind.Utc);
+ var jobId = connection.CreateExpiredJob(Job.FromExpression(() => SampleMethod("Hello")),
+ new Dictionary { { "Key1", "Value1" }, { "Key2", "Value2" } },
+ createdAt,
+ TimeSpan.FromDays(1));
+
+ Assert.NotNull(jobId);
+ Assert.NotEmpty(jobId);
+
+ var databaseJob = database.Job.FindAll().Single();
+ Assert.Equal(jobId, databaseJob.Id.ToString());
+ Assert.Equal(createdAt, databaseJob.CreatedAt);
+ Assert.Equal(ObjectId.Empty, databaseJob.StateId);
+ Assert.Equal(null, databaseJob.StateName);
+
+ var invocationData = JobHelper.FromJson((string)databaseJob.InvocationData);
+ invocationData.Arguments = databaseJob.Arguments;
+
+ var job = invocationData.Deserialize();
+ Assert.Equal(typeof(MongoConnectionFacts), job.Type);
+ Assert.Equal("SampleMethod", job.Method.Name);
+ Assert.Equal("\"Hello\"", job.Arguments[0]);
+
+ Assert.True(createdAt.AddDays(1).AddMinutes(-1) < databaseJob.ExpireAt);
+ Assert.True(databaseJob.ExpireAt < createdAt.AddDays(1).AddMinutes(1));
+
+ var parameters = database.JobParameter.Find(Query.EQ(_ => _.JobId, int.Parse(jobId)))
+ .ToDictionary(x => x.Name, x => x.Value);
+
+ Assert.Equal("Value1", parameters["Key1"]);
+ Assert.Equal("Value2", parameters["Key2"]);
+ });
+ }
+
+ [Fact, CleanDatabase]
+ public void GetJobData_ThrowsAnException_WhenJobIdIsNull()
+ {
+ UseConnection((database, connection) => Assert.Throws(
+ () => connection.GetJobData(null)));
+ }
+
+ [Fact, CleanDatabase]
+ public void GetJobData_ReturnsNull_WhenThereIsNoSuchJob()
+ {
+ UseConnection((database, connection) =>
+ {
+ var result = connection.GetJobData("547527b4c6b6cc26a02d021d");
+ Assert.Null(result);
+ });
+ }
+
+ [Fact, CleanDatabase]
+ public void GetJobData_ReturnsResult_WhenJobExists()
+ {
+ UseConnection((database, connection) =>
+ {
+ var job = Job.FromExpression(() => SampleMethod("wrong"));
+
+ var jobDto = new JobDto
+ {
+ Id = 1,
+ InvocationData = JobHelper.ToJson(InvocationData.Serialize(job)),
+ Arguments = "['Arguments']",
+ StateName = "Succeeded",
+ CreatedAt = database.GetServerTimeUtc()
+ };
+ database.Job.Insert(jobDto);
+
+ var result = connection.GetJobData(jobDto.Id.ToString());
+
+ Assert.NotNull(result);
+ Assert.NotNull(result.Job);
+ Assert.Equal("Succeeded", result.State);
+ Assert.Equal("Arguments", result.Job.Arguments[0]);
+ Assert.Null(result.LoadException);
+ Assert.True(database.GetServerTimeUtc().AddMinutes(-1) < result.CreatedAt);
+ Assert.True(result.CreatedAt < DateTime.UtcNow.AddMinutes(1));
+ });
+ }
+
+ [Fact, CleanDatabase]
+ public void GetStateData_ThrowsAnException_WhenJobIdIsNull()
+ {
+ UseConnection(
+ (database, connection) => Assert.Throws(
+ () => connection.GetStateData(null)));
+ }
+
+ [Fact, CleanDatabase]
+ public void GetStateData_ReturnsNull_IfThereIsNoSuchState()
+ {
+ UseConnection((database, connection) =>
+ {
+ var result = connection.GetStateData("547527b4c6b6cc26a02d021d");
+ Assert.Null(result);
+ });
+ }
+
+ [Fact, CleanDatabase]
+ public void GetStateData_ReturnsCorrectData()
+ {
+ UseConnection((database, connection) =>
+ {
+ var data = new Dictionary
+ {
+ { "Key", "Value" }
+ };
+
+ var jobDto = new JobDto
+ {
+ Id = 1,
+ InvocationData = "",
+ Arguments = "",
+ StateName = "",
+ CreatedAt = database.GetServerTimeUtc()
+ };
+
+ database.Job.Insert(jobDto);
+ var jobId = jobDto.Id;
+
+ database.State.Insert(new StateDto
+ {
+ Id = ObjectId.GenerateNewId(),
+ JobId = jobId,
+ Name = "old-state",
+ CreatedAt = database.GetServerTimeUtc()
+ });
+
+ var stateDto = new StateDto
+ {
+ Id = ObjectId.GenerateNewId(),
+ JobId = jobId,
+ Name = "Name",
+ Reason = "Reason",
+ Data = JobHelper.ToJson(data),
+ CreatedAt = database.GetServerTimeUtc()
+ };
+ database.State.Insert(stateDto);
+
+ jobDto.StateId = stateDto.Id;
+ database.Job.Save(jobDto);
+
+ var result = connection.GetStateData(jobId.ToString());
+ Assert.NotNull(result);
+
+ Assert.Equal("Name", result.Name);
+ Assert.Equal("Reason", result.Reason);
+ Assert.Equal("Value", result.Data["Key"]);
+ });
+ }
+
+ [Fact, CleanDatabase]
+ public void GetJobData_ReturnsJobLoadException_IfThereWasADeserializationException()
+ {
+ UseConnection((database, connection) =>
+ {
+ var jobDto = new JobDto
+ {
+ Id = 1,
+ InvocationData = JobHelper.ToJson(new InvocationData(null, null, null, null)),
+ Arguments = "['Arguments']",
+ StateName = "Succeeded",
+ CreatedAt = database.GetServerTimeUtc()
+ };
+ database.Job.Insert(jobDto);
+ var jobId = jobDto.Id;
+
+ var result = connection.GetJobData(jobId.ToString());
+
+ Assert.NotNull(result.LoadException);
+ });
+ }
+
+ [Fact, CleanDatabase]
+ public void SetParameter_ThrowsAnException_WhenJobIdIsNull()
+ {
+ UseConnection((database, connection) =>
+ {
+ var exception = Assert.Throws(
+ () => connection.SetJobParameter(null, "name", "value"));
+
+ Assert.Equal("id", exception.ParamName);
+ });
+ }
+
+ [Fact, CleanDatabase]
+ public void SetParameter_ThrowsAnException_WhenNameIsNull()
+ {
+ UseConnection((database, connection) =>
+ {
+ var exception = Assert.Throws(
+ () => connection.SetJobParameter("547527b4c6b6cc26a02d021d", null, "value"));
+
+ Assert.Equal("name", exception.ParamName);
+ });
+ }
+
+ [Fact, CleanDatabase]
+ public void SetParameters_CreatesNewParameter_WhenParameterWithTheGivenNameDoesNotExists()
+ {
+ UseConnection((database, connection) =>
+ {
+ var jobDto = new JobDto
+ {
+ Id = 1,
+ InvocationData = "",
+ Arguments = "",
+ CreatedAt = database.GetServerTimeUtc()
+ };
+ database.Job.Insert(jobDto);
+ string jobId = jobDto.Id.ToString();
+
+ connection.SetJobParameter(jobId, "Name", "Value");
+
+ var parameter = database.JobParameter.FindOne(Query.And(Query.EQ(_ => _.JobId, int.Parse(jobId)),
+ Query.EQ(_ => _.Name, "Name")));
+
+ Assert.Equal("Value", parameter.Value);
+ });
+ }
+
+ [Fact, CleanDatabase]
+ public void SetParameter_UpdatesValue_WhenParameterWithTheGivenName_AlreadyExists()
+ {
+ UseConnection((database, connection) =>
+ {
+ var jobDto = new JobDto
+ {
+ Id = 1,
+ InvocationData = "",
+ Arguments = "",
+ CreatedAt = database.GetServerTimeUtc()
+ };
+ database.Job.Insert(jobDto);
+ string jobId = jobDto.Id.ToString();
+
+ connection.SetJobParameter(jobId, "Name", "Value");
+ connection.SetJobParameter(jobId, "Name", "AnotherValue");
+
+ var parameter = database.JobParameter.FindOne(Query.And(Query.EQ(_ => _.JobId, int.Parse(jobId)),
+ Query.EQ(_ => _.Name, "Name")));
+
+ Assert.Equal("AnotherValue", parameter.Value);
+ });
+ }
+
+ [Fact, CleanDatabase]
+ public void SetParameter_CanAcceptNulls_AsValues()
+ {
+ UseConnection((database, connection) =>
+ {
+ var jobDto = new JobDto
+ {
+ Id = 1,
+ InvocationData = "",
+ Arguments = "",
+ CreatedAt = database.GetServerTimeUtc()
+ };
+ database.Job.Insert(jobDto);
+ string jobId = jobDto.Id.ToString();
+
+ connection.SetJobParameter(jobId, "Name", null);
+
+ var parameter = database.JobParameter.FindOne(Query.And(Query.EQ(_ => _.JobId, int.Parse(jobId)),
+ Query.EQ(_ => _.Name, "Name")));
+
+ Assert.Equal(null, parameter.Value);
+ });
+ }
+
+ [Fact, CleanDatabase]
+ public void GetParameter_ThrowsAnException_WhenJobIdIsNull()
+ {
+ UseConnection((database, connection) =>
+ {
+ var exception = Assert.Throws(
+ () => connection.GetJobParameter(null, "hello"));
+
+ Assert.Equal("id", exception.ParamName);
+ });
+ }
+
+ [Fact, CleanDatabase]
+ public void GetParameter_ThrowsAnException_WhenNameIsNull()
+ {
+ UseConnection((database, connection) =>
+ {
+ var exception = Assert.Throws(
+ () => connection.GetJobParameter("547527b4c6b6cc26a02d021d", null));
+
+ Assert.Equal("name", exception.ParamName);
+ });
+ }
+
+ [Fact, CleanDatabase]
+ public void GetParameter_ReturnsNull_WhenParameterDoesNotExists()
+ {
+ UseConnection((database, connection) =>
+ {
+ var value = connection.GetJobParameter("1", "hello");
+ Assert.Null(value);
+ });
+ }
+
+ [Fact, CleanDatabase]
+ public void GetParameter_ReturnsParameterValue_WhenJobExists()
+ {
+ UseConnection((database, connection) =>
+ {
+ var jobDto = new JobDto
+ {
+ Id = 1,
+ InvocationData = "",
+ Arguments = "",
+ CreatedAt = database.GetServerTimeUtc()
+ };
+ database.Job.Insert(jobDto);
+ string jobId = jobDto.Id.ToString();
+
+ database.JobParameter.Insert(new JobParameterDto
+ {
+ Id = ObjectId.GenerateNewId(),
+ JobId = int.Parse(jobId),
+ Name = "name",
+ Value = "value"
+ });
+
+ var value = connection.GetJobParameter(jobId, "name");
+
+ Assert.Equal("value", value);
+ });
+ }
+
+ [Fact, CleanDatabase]
+ public void GetFirstByLowestScoreFromSet_ThrowsAnException_WhenKeyIsNull()
+ {
+ UseConnection((database, connection) =>
+ {
+ var exception = Assert.Throws(
+ () => connection.GetFirstByLowestScoreFromSet(null, 0, 1));
+
+ Assert.Equal("key", exception.ParamName);
+ });
+ }
+
+ [Fact, CleanDatabase]
+ public void GetFirstByLowestScoreFromSet_ThrowsAnException_ToScoreIsLowerThanFromScore()
+ {
+ UseConnection((database, connection) => Assert.Throws(
+ () => connection.GetFirstByLowestScoreFromSet("key", 0, -1)));
+ }
+
+ [Fact, CleanDatabase]
+ public void GetFirstByLowestScoreFromSet_ReturnsNull_WhenTheKeyDoesNotExist()
+ {
+ UseConnection((database, connection) =>
+ {
+ var result = connection.GetFirstByLowestScoreFromSet(
+ "key", 0, 1);
+
+ Assert.Null(result);
+ });
+ }
+
+ [Fact, CleanDatabase]
+ public void GetFirstByLowestScoreFromSet_ReturnsTheValueWithTheLowestScore()
+ {
+ UseConnection((database, connection) =>
+ {
+ database.Set.Insert(new SetDto
+ {
+ Id = ObjectId.GenerateNewId(),
+ Key = "key",
+ Score = 1.0,
+ Value = "1.0"
+ });
+ database.Set.Insert(new SetDto
+ {
+ Id = ObjectId.GenerateNewId(),
+ Key = "key",
+ Score = -1.0,
+ Value = "-1.0"
+ });
+ database.Set.Insert(new SetDto
+ {
+ Id = ObjectId.GenerateNewId(),
+ Key = "key",
+ Score = -5.0,
+ Value = "-5.0"
+ });
+ database.Set.Insert(new SetDto
+ {
+ Id = ObjectId.GenerateNewId(),
+ Key = "another-key",
+ Score = -2.0,
+ Value = "-2.0"
+ });
+
+ var result = connection.GetFirstByLowestScoreFromSet("key", -1.0, 3.0);
+
+ Assert.Equal("-1.0", result);
+ });
+ }
+
+ [Fact, CleanDatabase]
+ public void AnnounceServer_ThrowsAnException_WhenServerIdIsNull()
+ {
+ UseConnection((database, connection) =>
+ {
+ var exception = Assert.Throws(
+ () => connection.AnnounceServer(null, new ServerContext()));
+
+ Assert.Equal("serverId", exception.ParamName);
+ });
+ }
+
+ [Fact, CleanDatabase]
+ public void AnnounceServer_ThrowsAnException_WhenContextIsNull()
+ {
+ UseConnection((database, connection) =>
+ {
+ var exception = Assert.Throws(
+ () => connection.AnnounceServer("server", null));
+
+ Assert.Equal("context", exception.ParamName);
+ });
+ }
+
+ [Fact, CleanDatabase]
+ public void AnnounceServer_CreatesOrUpdatesARecord()
+ {
+ UseConnection((database, connection) =>
+ {
+ var context1 = new ServerContext
+ {
+ Queues = new[] { "critical", "default" },
+ WorkerCount = 4
+ };
+ connection.AnnounceServer("server", context1);
+
+ var server = database.Server.FindAll().Single();
+ Assert.Equal("server", server.Id);
+ Assert.True(((string)server.Data).StartsWith(
+ "{\"WorkerCount\":4,\"Queues\":[\"critical\",\"default\"],\"StartedAt\":"),
+ server.Data);
+ Assert.NotNull(server.LastHeartbeat);
+
+ var context2 = new ServerContext
+ {
+ Queues = new[] { "default" },
+ WorkerCount = 1000
+ };
+ connection.AnnounceServer("server", context2);
+ var sameServer = database.Server.FindAll().Single();
+ Assert.Equal("server", sameServer.Id);
+ Assert.Contains("1000", sameServer.Data);
+ });
+ }
+
+ [Fact, CleanDatabase]
+ public void RemoveServer_ThrowsAnException_WhenServerIdIsNull()
+ {
+ UseConnection((database, connection) => Assert.Throws(
+ () => connection.RemoveServer(null)));
+ }
+
+ [Fact, CleanDatabase]
+ public void RemoveServer_RemovesAServerRecord()
+ {
+ UseConnection((database, connection) =>
+ {
+ database.Server.Insert(new ServerDto
+ {
+ Id = "Server1",
+ Data = "",
+ LastHeartbeat = database.GetServerTimeUtc()
+ });
+ database.Server.Insert(new ServerDto
+ {
+ Id = "Server2",
+ Data = "",
+ LastHeartbeat = database.GetServerTimeUtc()
+ });
+
+ connection.RemoveServer("Server1");
+
+ var server = database.Server.FindAll().Single();
+ Assert.NotEqual("Server1", server.Id, StringComparer.OrdinalIgnoreCase);
+ });
+ }
+
+ [Fact, CleanDatabase]
+ public void Heartbeat_ThrowsAnException_WhenServerIdIsNull()
+ {
+ UseConnection((database, connection) => Assert.Throws(
+ () => connection.Heartbeat(null)));
+ }
+
+ [Fact, CleanDatabase]
+ public void Heartbeat_UpdatesLastHeartbeat_OfTheServerWithGivenId()
+ {
+ UseConnection((database, connection) =>
+ {
+ database.Server.Insert(new ServerDto
+ {
+ Id = "server1",
+ Data = "",
+ LastHeartbeat = new DateTime(2012, 12, 12, 12, 12, 12, DateTimeKind.Utc)
+ });
+ database.Server.Insert(new ServerDto
+ {
+ Id = "server2",
+ Data = "",
+ LastHeartbeat = new DateTime(2012, 12, 12, 12, 12, 12, DateTimeKind.Utc)
+ });
+
+ connection.Heartbeat("server1");
+
+ var servers = database.Server.FindAll()
+ .ToDictionary(x => x.Id, x => x.LastHeartbeat);
+
+ Assert.NotEqual(2012, servers["server1"].Year);
+ Assert.Equal(2012, servers["server2"].Year);
+ });
+ }
+
+ [Fact, CleanDatabase]
+ public void RemoveTimedOutServers_ThrowsAnException_WhenTimeOutIsNegative()
+ {
+ UseConnection((database, connection) => Assert.Throws