├── .editorconfig
├── .gitignore
├── .nuget
├── NuGet.exe
├── packages.config
└── packages.lock.json
├── COPYING
├── COPYING.LESSER
├── Directory.Build.props
├── Hangfire.InMemory.sln
├── Hangfire.InMemory.sln.DotSettings
├── LICENSE.md
├── LICENSE_ROYALTYFREE
├── LICENSE_STANDARD
├── NuGet.config
├── README.md
├── appveyor.yml
├── build.bat
├── nuspecs
├── Hangfire.InMemory.nuspec
└── icon.png
├── psake-project.ps1
├── samples
└── ConsoleSample
│ ├── ConsoleSample.csproj
│ ├── HarnessHostedService.cs
│ ├── Program.cs
│ ├── Properties
│ └── launchSettings.json
│ ├── Startup.cs
│ ├── appsettings.Development.json
│ ├── appsettings.json
│ └── packages.lock.json
├── src
├── Directory.Build.props
├── Hangfire.InMemory
│ ├── Entities
│ │ ├── CounterEntry.cs
│ │ ├── ExpirableEntryComparer.cs
│ │ ├── HashEntry.cs
│ │ ├── IExpirableEntry.cs
│ │ ├── JobEntry.cs
│ │ ├── JobStateCreatedAtComparer.cs
│ │ ├── ListEntry.cs
│ │ ├── LockEntry.cs
│ │ ├── QueueEntry.cs
│ │ ├── QueueWaitNode.cs
│ │ ├── ServerEntry.cs
│ │ ├── SetEntry.cs
│ │ ├── SortedSetItem.cs
│ │ ├── SortedSetItemComparer.cs
│ │ └── StateRecord.cs
│ ├── GlobalConfigurationExtensions.cs
│ ├── Hangfire.InMemory.csproj
│ ├── IKeyProvider.cs
│ ├── InMemoryConnection.cs
│ ├── InMemoryFetchedJob.cs
│ ├── InMemoryMonitoringApi.cs
│ ├── InMemoryStorage.cs
│ ├── InMemoryStorageIdType.cs
│ ├── InMemoryStorageOptions.cs
│ ├── InMemoryTransaction.cs
│ ├── JobStorageMonitor.cs
│ ├── Properties
│ │ └── AssemblyInfo.cs
│ ├── PublicAPI.Shipped.txt
│ ├── PublicAPI.Unshipped.txt
│ ├── State
│ │ ├── Commands.cs
│ │ ├── Dispatcher.cs
│ │ ├── DispatcherBase.cs
│ │ ├── DispatcherCallback.cs
│ │ ├── DispatcherExtensions.cs
│ │ ├── ExtensionMethods.cs
│ │ ├── ICommand.cs
│ │ ├── IDispatcherCallback.cs
│ │ ├── MemoryState.cs
│ │ ├── MonitoringQueries.cs
│ │ ├── MonotonicTime.cs
│ │ └── Queries.cs
│ └── packages.lock.json
└── SharedAssemblyInfo.cs
└── tests
├── Directory.Build.props
└── Hangfire.InMemory.Tests
├── Entities
├── ExpirableEntryComparerFacts.cs
├── JobStateCreatedAtComparerFacts.cs
├── LockEntryFacts.cs
└── SortedSetItemComparerFacts.cs
├── GlobalConfigurationExtensionsFacts.cs
├── Hangfire.InMemory.Tests.csproj
├── ITestServices.cs
├── InMemoryConnectionFacts.cs
├── InMemoryDispatcherBaseFacts.cs
├── InMemoryFetchedJobFacts.cs
├── InMemoryMonitoringApiFacts.cs
├── InMemoryStorageFacts.cs
├── InMemoryTransactionFacts.cs
├── MonotonicTimeFacts.cs
├── State
└── DispatcherFacts.cs
├── StringKeyProvider.cs
├── TestInMemoryDispatcher.cs
└── packages.lock.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*.cs,*.ps1]
4 | indent_style = space
5 | indent_size = 4
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | .idea/
7 |
8 | # User-specific files
9 | *.rsuser
10 | *.suo
11 | *.user
12 | *.userosscache
13 | *.sln.docstates
14 |
15 | # User-specific files (MonoDevelop/Xamarin Studio)
16 | *.userprefs
17 |
18 | # Mono auto generated files
19 | mono_crash.*
20 |
21 | # Build results
22 | build/
23 | [Dd]ebug/
24 | [Dd]ebugPublic/
25 | [Rr]elease/
26 | [Rr]eleases/
27 | x64/
28 | x86/
29 | [Aa][Rr][Mm]/
30 | [Aa][Rr][Mm]64/
31 | bld/
32 | [Bb]in/
33 | [Oo]bj/
34 | [Ll]og/
35 | [Ll]ogs/
36 |
37 | # Visual Studio 2015/2017 cache/options directory
38 | .vs/
39 | # Uncomment if you have tasks that create the project's static files in wwwroot
40 | #wwwroot/
41 |
42 | # Visual Studio 2017 auto generated files
43 | Generated\ Files/
44 |
45 | # MSTest test Results
46 | [Tt]est[Rr]esult*/
47 | [Bb]uild[Ll]og.*
48 |
49 | # NUnit
50 | *.VisualState.xml
51 | TestResult.xml
52 | nunit-*.xml
53 |
54 | # Build Results of an ATL Project
55 | [Dd]ebugPS/
56 | [Rr]eleasePS/
57 | dlldata.c
58 |
59 | # Benchmark Results
60 | BenchmarkDotNet.Artifacts/
61 |
62 | # .NET Core
63 | project.lock.json
64 | project.fragment.lock.json
65 | artifacts/
66 |
67 | # StyleCop
68 | StyleCopReport.xml
69 |
70 | # Files built by Visual Studio
71 | *_i.c
72 | *_p.c
73 | *_h.h
74 | *.ilk
75 | *.meta
76 | *.obj
77 | *.iobj
78 | *.pch
79 | *.pdb
80 | *.ipdb
81 | *.pgc
82 | *.pgd
83 | *.rsp
84 | *.sbr
85 | *.tlb
86 | *.tli
87 | *.tlh
88 | *.tmp
89 | *.tmp_proj
90 | *_wpftmp.csproj
91 | *.log
92 | *.vspscc
93 | *.vssscc
94 | .builds
95 | *.pidb
96 | *.svclog
97 | *.scc
98 |
99 | # Chutzpah Test files
100 | _Chutzpah*
101 |
102 | # Visual C++ cache files
103 | ipch/
104 | *.aps
105 | *.ncb
106 | *.opendb
107 | *.opensdf
108 | *.sdf
109 | *.cachefile
110 | *.VC.db
111 | *.VC.VC.opendb
112 |
113 | # Visual Studio profiler
114 | *.psess
115 | *.vsp
116 | *.vspx
117 | *.sap
118 |
119 | # Visual Studio Trace Files
120 | *.e2e
121 |
122 | # TFS 2012 Local Workspace
123 | $tf/
124 |
125 | # Guidance Automation Toolkit
126 | *.gpState
127 |
128 | # ReSharper is a .NET coding add-in
129 | _ReSharper*/
130 | *.[Rr]e[Ss]harper
131 | *.DotSettings.user
132 |
133 | # TeamCity is a build add-in
134 | _TeamCity*
135 |
136 | # DotCover is a Code Coverage Tool
137 | *.dotCover
138 |
139 | # AxoCover is a Code Coverage Tool
140 | .axoCover/*
141 | !.axoCover/settings.json
142 |
143 | # Visual Studio code coverage results
144 | *.coverage
145 | *.coveragexml
146 |
147 | # NCrunch
148 | _NCrunch_*
149 | .*crunch*.local.xml
150 | nCrunchTemp_*
151 |
152 | # MightyMoose
153 | *.mm.*
154 | AutoTest.Net/
155 |
156 | # Web workbench (sass)
157 | .sass-cache/
158 |
159 | # Installshield output folder
160 | [Ee]xpress/
161 |
162 | # DocProject is a documentation generator add-in
163 | DocProject/buildhelp/
164 | DocProject/Help/*.HxT
165 | DocProject/Help/*.HxC
166 | DocProject/Help/*.hhc
167 | DocProject/Help/*.hhk
168 | DocProject/Help/*.hhp
169 | DocProject/Help/Html2
170 | DocProject/Help/html
171 |
172 | # Click-Once directory
173 | publish/
174 |
175 | # Publish Web Output
176 | *.[Pp]ublish.xml
177 | *.azurePubxml
178 | # Note: Comment the next line if you want to checkin your web deploy settings,
179 | # but database connection strings (with potential passwords) will be unencrypted
180 | *.pubxml
181 | *.publishproj
182 |
183 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
184 | # checkin your Azure Web App publish settings, but sensitive information contained
185 | # in these scripts will be unencrypted
186 | PublishScripts/
187 |
188 | # NuGet Packages
189 | *.nupkg
190 | # NuGet Symbol Packages
191 | *.snupkg
192 | # The packages folder can be ignored because of Package Restore
193 | **/[Pp]ackages/*
194 | # except build/, which is used as an MSBuild target.
195 | !**/[Pp]ackages/build/
196 | # Uncomment if necessary however generally it will be regenerated when needed
197 | #!**/[Pp]ackages/repositories.config
198 | # NuGet v3's project.json files produces more ignorable files
199 | *.nuget.props
200 | *.nuget.targets
201 |
202 | # Microsoft Azure Build Output
203 | csx/
204 | *.build.csdef
205 |
206 | # Microsoft Azure Emulator
207 | ecf/
208 | rcf/
209 |
210 | # Windows Store app package directories and files
211 | AppPackages/
212 | BundleArtifacts/
213 | Package.StoreAssociation.xml
214 | _pkginfo.txt
215 | *.appx
216 | *.appxbundle
217 | *.appxupload
218 |
219 | # Visual Studio cache files
220 | # files ending in .cache can be ignored
221 | *.[Cc]ache
222 | # but keep track of directories ending in .cache
223 | !?*.[Cc]ache/
224 |
225 | # Others
226 | ClientBin/
227 | ~$*
228 | *~
229 | *.dbmdl
230 | *.dbproj.schemaview
231 | *.jfm
232 | *.pfx
233 | *.publishsettings
234 | orleans.codegen.cs
235 |
236 | # Including strong name files can present a security risk
237 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
238 | #*.snk
239 |
240 | # Since there are multiple workflows, uncomment next line to ignore bower_components
241 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
242 | #bower_components/
243 |
244 | # RIA/Silverlight projects
245 | Generated_Code/
246 |
247 | # Backup & report files from converting an old project file
248 | # to a newer Visual Studio version. Backup files are not needed,
249 | # because we have git ;-)
250 | _UpgradeReport_Files/
251 | Backup*/
252 | UpgradeLog*.XML
253 | UpgradeLog*.htm
254 | ServiceFabricBackup/
255 | *.rptproj.bak
256 |
257 | # SQL Server files
258 | *.mdf
259 | *.ldf
260 | *.ndf
261 |
262 | # Business Intelligence projects
263 | *.rdl.data
264 | *.bim.layout
265 | *.bim_*.settings
266 | *.rptproj.rsuser
267 | *- [Bb]ackup.rdl
268 | *- [Bb]ackup ([0-9]).rdl
269 | *- [Bb]ackup ([0-9][0-9]).rdl
270 |
271 | # Microsoft Fakes
272 | FakesAssemblies/
273 |
274 | # GhostDoc plugin setting file
275 | *.GhostDoc.xml
276 |
277 | # Node.js Tools for Visual Studio
278 | .ntvs_analysis.dat
279 | node_modules/
280 |
281 | # Visual Studio 6 build log
282 | *.plg
283 |
284 | # Visual Studio 6 workspace options file
285 | *.opt
286 |
287 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
288 | *.vbw
289 |
290 | # Visual Studio LightSwitch build output
291 | **/*.HTMLClient/GeneratedArtifacts
292 | **/*.DesktopClient/GeneratedArtifacts
293 | **/*.DesktopClient/ModelManifest.xml
294 | **/*.Server/GeneratedArtifacts
295 | **/*.Server/ModelManifest.xml
296 | _Pvt_Extensions
297 |
298 | # Paket dependency manager
299 | .paket/paket.exe
300 | paket-files/
301 |
302 | # FAKE - F# Make
303 | .fake/
304 |
305 | # CodeRush personal settings
306 | .cr/personal
307 |
308 | # Python Tools for Visual Studio (PTVS)
309 | __pycache__/
310 | *.pyc
311 |
312 | # Cake - Uncomment if you are using it
313 | # tools/**
314 | # !tools/packages.config
315 |
316 | # Tabs Studio
317 | *.tss
318 |
319 | # Telerik's JustMock configuration file
320 | *.jmconfig
321 |
322 | # BizTalk build output
323 | *.btp.cs
324 | *.btm.cs
325 | *.odx.cs
326 | *.xsd.cs
327 |
328 | # OpenCover UI analysis results
329 | OpenCover/
330 |
331 | # Azure Stream Analytics local run output
332 | ASALocalRun/
333 |
334 | # MSBuild Binary and Structured Log
335 | *.binlog
336 |
337 | # NVidia Nsight GPU debugger configuration file
338 | *.nvuser
339 |
340 | # MFractors (Xamarin productivity tool) working folder
341 | .mfractor/
342 |
343 | # Local History for Visual Studio
344 | .localhistory/
345 |
346 | # BeatPulse healthcheck temp database
347 | healthchecksdb
348 |
349 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
350 | MigrationBackup/
351 |
352 | # Ionide (cross platform F# VS Code tools) working folder
353 | .ionide/
354 |
--------------------------------------------------------------------------------
/.nuget/NuGet.exe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HangfireIO/Hangfire.InMemory/a68f39353edae4b23a4c0f5d61378ba8d1bb06ed/.nuget/NuGet.exe
--------------------------------------------------------------------------------
/.nuget/packages.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.nuget/packages.lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 1,
3 | "dependencies": {
4 | "Any,Version=v0.0": {
5 | "Hangfire.Build": {
6 | "type": "Direct",
7 | "requested": "[0.5.0, 0.5.0]",
8 | "resolved": "0.5.0",
9 | "contentHash": "4yRCdMaDr6cyFRmCvpFO8kBMV57KPOofugaHOsjkDEDw+G/BCGWOdrpXfkAeTEtZBPUv2jS0PYmVNK5680KxXQ=="
10 | },
11 | "psake": {
12 | "type": "Direct",
13 | "requested": "[4.4.1, 4.4.1]",
14 | "resolved": "4.4.1",
15 | "contentHash": "Hn5kdGPEoapi+wAAjaGjKEZVnuYp7fUrPK3IivLYG6Bn4adhd8l+KXXPMEmte41RmrLvfV7XGZa9KsSTc0gjDA=="
16 | }
17 | }
18 | }
19 | }
--------------------------------------------------------------------------------
/COPYING.LESSER:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 |
9 | This version of the GNU Lesser General Public License incorporates
10 | the terms and conditions of version 3 of the GNU General Public
11 | License, supplemented by the additional permissions listed below.
12 |
13 | 0. Additional Definitions.
14 |
15 | As used herein, "this License" refers to version 3 of the GNU Lesser
16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU
17 | General Public License.
18 |
19 | "The Library" refers to a covered work governed by this License,
20 | other than an Application or a Combined Work as defined below.
21 |
22 | An "Application" is any work that makes use of an interface provided
23 | by the Library, but which is not otherwise based on the Library.
24 | Defining a subclass of a class defined by the Library is deemed a mode
25 | of using an interface provided by the Library.
26 |
27 | A "Combined Work" is a work produced by combining or linking an
28 | Application with the Library. The particular version of the Library
29 | with which the Combined Work was made is also called the "Linked
30 | Version".
31 |
32 | The "Minimal Corresponding Source" for a Combined Work means the
33 | Corresponding Source for the Combined Work, excluding any source code
34 | for portions of the Combined Work that, considered in isolation, are
35 | based on the Application, and not on the Linked Version.
36 |
37 | The "Corresponding Application Code" for a Combined Work means the
38 | object code and/or source code for the Application, including any data
39 | and utility programs needed for reproducing the Combined Work from the
40 | Application, but excluding the System Libraries of the Combined Work.
41 |
42 | 1. Exception to Section 3 of the GNU GPL.
43 |
44 | You may convey a covered work under sections 3 and 4 of this License
45 | without being bound by section 3 of the GNU GPL.
46 |
47 | 2. Conveying Modified Versions.
48 |
49 | If you modify a copy of the Library, and, in your modifications, a
50 | facility refers to a function or data to be supplied by an Application
51 | that uses the facility (other than as an argument passed when the
52 | facility is invoked), then you may convey a copy of the modified
53 | version:
54 |
55 | a) under this License, provided that you make a good faith effort to
56 | ensure that, in the event an Application does not supply the
57 | function or data, the facility still operates, and performs
58 | whatever part of its purpose remains meaningful, or
59 |
60 | b) under the GNU GPL, with none of the additional permissions of
61 | this License applicable to that copy.
62 |
63 | 3. Object Code Incorporating Material from Library Header Files.
64 |
65 | The object code form of an Application may incorporate material from
66 | a header file that is part of the Library. You may convey such object
67 | code under terms of your choice, provided that, if the incorporated
68 | material is not limited to numerical parameters, data structure
69 | layouts and accessors, or small macros, inline functions and templates
70 | (ten or fewer lines in length), you do both of the following:
71 |
72 | a) Give prominent notice with each copy of the object code that the
73 | Library is used in it and that the Library and its use are
74 | covered by this License.
75 |
76 | b) Accompany the object code with a copy of the GNU GPL and this license
77 | document.
78 |
79 | 4. Combined Works.
80 |
81 | You may convey a Combined Work under terms of your choice that,
82 | taken together, effectively do not restrict modification of the
83 | portions of the Library contained in the Combined Work and reverse
84 | engineering for debugging such modifications, if you also do each of
85 | the following:
86 |
87 | a) Give prominent notice with each copy of the Combined Work that
88 | the Library is used in it and that the Library and its use are
89 | covered by this License.
90 |
91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license
92 | document.
93 |
94 | c) For a Combined Work that displays copyright notices during
95 | execution, include the copyright notice for the Library among
96 | these notices, as well as a reference directing the user to the
97 | copies of the GNU GPL and this license document.
98 |
99 | d) Do one of the following:
100 |
101 | 0) Convey the Minimal Corresponding Source under the terms of this
102 | License, and the Corresponding Application Code in a form
103 | suitable for, and under terms that permit, the user to
104 | recombine or relink the Application with a modified version of
105 | the Linked Version to produce a modified Combined Work, in the
106 | manner specified by section 6 of the GNU GPL for conveying
107 | Corresponding Source.
108 |
109 | 1) Use a suitable shared library mechanism for linking with the
110 | Library. A suitable mechanism is one that (a) uses at run time
111 | a copy of the Library already present on the user's computer
112 | system, and (b) will operate properly with a modified version
113 | of the Library that is interface-compatible with the Linked
114 | Version.
115 |
116 | e) Provide Installation Information, but only if you would otherwise
117 | be required to provide such information under section 6 of the
118 | GNU GPL, and only to the extent that such information is
119 | necessary to install and execute a modified version of the
120 | Combined Work produced by recombining or relinking the
121 | Application with a modified version of the Linked Version. (If
122 | you use option 4d0, the Installation Information must accompany
123 | the Minimal Corresponding Source and Corresponding Application
124 | Code. If you use option 4d1, you must provide the Installation
125 | Information in the manner specified by section 6 of the GNU GPL
126 | for conveying Corresponding Source.)
127 |
128 | 5. Combined Libraries.
129 |
130 | You may place library facilities that are a work based on the
131 | Library side by side in a single library together with other library
132 | facilities that are not Applications and are not covered by this
133 | License, and convey such a combined library under terms of your
134 | choice, if you do both of the following:
135 |
136 | a) Accompany the combined library with a copy of the same work based
137 | on the Library, uncombined with any other library facilities,
138 | conveyed under the terms of this License.
139 |
140 | b) Give prominent notice with the combined library that part of it
141 | is a work based on the Library, and explaining where to find the
142 | accompanying uncombined form of the same work.
143 |
144 | 6. Revised Versions of the GNU Lesser General Public License.
145 |
146 | The Free Software Foundation may publish revised and/or new versions
147 | of the GNU Lesser General Public License from time to time. Such new
148 | versions will be similar in spirit to the present version, but may
149 | differ in detail to address new problems or concerns.
150 |
151 | Each version is given a distinguishing version number. If the
152 | Library as you received it specifies that a certain numbered version
153 | of the GNU Lesser General Public License "or any later version"
154 | applies to it, you have the option of following the terms and
155 | conditions either of that published version or of any later version
156 | published by the Free Software Foundation. If the Library as you
157 | received it does not specify a version number of the GNU Lesser
158 | General Public License, you may choose any version of the GNU Lesser
159 | General Public License ever published by the Free Software Foundation.
160 |
161 | If the Library as you received it specifies that a proxy can decide
162 | whether future versions of the GNU Lesser General Public License shall
163 | apply, that proxy's public statement of acceptance of any version is
164 | permanent authorization for you to choose that version for the
165 | Library.
166 |
--------------------------------------------------------------------------------
/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 | true
4 | true
5 |
6 |
7 |
8 | true
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Hangfire.InMemory.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.30413.136
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hangfire.InMemory", "src\Hangfire.InMemory\Hangfire.InMemory.csproj", "{DB32B42E-4351-4494-B4DA-8F975E36D1FF}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleSample", "samples\ConsoleSample\ConsoleSample.csproj", "{73938AE5-60E5-499B-9A07-6BB813E8BD31}"
9 | EndProject
10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hangfire.InMemory.Tests", "tests\Hangfire.InMemory.Tests\Hangfire.InMemory.Tests.csproj", "{1BF7E419-B80C-45D2-9FF9-600C0151F98B}"
11 | EndProject
12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "nuspecs", "nuspecs", "{93F80517-8600-4875-9AE9-EFA28627C7CE}"
13 | ProjectSection(SolutionItems) = preProject
14 | nuspecs\Hangfire.InMemory.nuspec = nuspecs\Hangfire.InMemory.nuspec
15 | EndProjectSection
16 | EndProject
17 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{3FACAFD2-7337-4A49-A68B-06BABBAEF557}"
18 | ProjectSection(SolutionItems) = preProject
19 | appveyor.yml = appveyor.yml
20 | psake-project.ps1 = psake-project.ps1
21 | src\Directory.Build.props = src\Directory.Build.props
22 | README.md = README.md
23 | EndProjectSection
24 | EndProject
25 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{C029F26E-B8DB-4996-BF96-23A062EA172B}"
26 | ProjectSection(SolutionItems) = preProject
27 | tests\Directory.Build.props = tests\Directory.Build.props
28 | EndProjectSection
29 | EndProject
30 | Global
31 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
32 | Debug|Any CPU = Debug|Any CPU
33 | Release|Any CPU = Release|Any CPU
34 | EndGlobalSection
35 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
36 | {DB32B42E-4351-4494-B4DA-8F975E36D1FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
37 | {DB32B42E-4351-4494-B4DA-8F975E36D1FF}.Debug|Any CPU.Build.0 = Debug|Any CPU
38 | {DB32B42E-4351-4494-B4DA-8F975E36D1FF}.Release|Any CPU.ActiveCfg = Release|Any CPU
39 | {DB32B42E-4351-4494-B4DA-8F975E36D1FF}.Release|Any CPU.Build.0 = Release|Any CPU
40 | {73938AE5-60E5-499B-9A07-6BB813E8BD31}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
41 | {73938AE5-60E5-499B-9A07-6BB813E8BD31}.Debug|Any CPU.Build.0 = Debug|Any CPU
42 | {73938AE5-60E5-499B-9A07-6BB813E8BD31}.Release|Any CPU.ActiveCfg = Release|Any CPU
43 | {73938AE5-60E5-499B-9A07-6BB813E8BD31}.Release|Any CPU.Build.0 = Release|Any CPU
44 | {1BF7E419-B80C-45D2-9FF9-600C0151F98B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
45 | {1BF7E419-B80C-45D2-9FF9-600C0151F98B}.Debug|Any CPU.Build.0 = Debug|Any CPU
46 | {1BF7E419-B80C-45D2-9FF9-600C0151F98B}.Release|Any CPU.ActiveCfg = Release|Any CPU
47 | {1BF7E419-B80C-45D2-9FF9-600C0151F98B}.Release|Any CPU.Build.0 = Release|Any CPU
48 | EndGlobalSection
49 | GlobalSection(SolutionProperties) = preSolution
50 | HideSolutionNode = FALSE
51 | EndGlobalSection
52 | GlobalSection(ExtensibilityGlobals) = postSolution
53 | SolutionGuid = {CCDF5746-29BA-47EF-8992-C08194F24A7B}
54 | EndGlobalSection
55 | GlobalSection(NestedProjects) = preSolution
56 | {1BF7E419-B80C-45D2-9FF9-600C0151F98B} = {C029F26E-B8DB-4996-BF96-23A062EA172B}
57 | EndGlobalSection
58 | EndGlobal
59 |
--------------------------------------------------------------------------------
/Hangfire.InMemory.sln.DotSettings:
--------------------------------------------------------------------------------
1 |
2 | True
3 | True
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | License
2 | ========
3 |
4 | Copyright © 2020 Hangfire OÜ.
5 |
6 | Hangfire software is an open-source software that is multi-licensed under the terms of the licenses listed in this file. Recipients may choose the terms under which they are want to use or distribute the software, when all the preconditions of a chosen license are satisfied.
7 |
8 | LGPL v3 License
9 | ---------------
10 |
11 | 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.
12 |
13 | 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.
14 |
15 | Please see COPYING.LESSER and COPYING files for details.
16 |
17 | Commercial License
18 | ------------------
19 |
20 | Subject to the purchase of a corresponding subscription (please see https://www.hangfire.io/pricing/), you may distribute Hangfire under the terms of commercial license, that allows you to distribute private forks and modifications. Please see LICENSE_STANDARD and LICENSE_ROYALTYFREE files for details.
21 |
--------------------------------------------------------------------------------
/NuGet.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | Microsoft;aspnet;odinserj;xunit;jamesnk;kzu;castleproject;psake
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Hangfire.InMemory
2 |
3 | [](https://www.nuget.org/packages/Hangfire.InMemory/) [](https://ci.appveyor.com/project/HangfireIO/hangfire-inmemory) [](https://sonarcloud.io/summary/new_code?id=HangfireIO_Hangfire.InMemory) [](https://sonarcloud.io/summary/new_code?id=HangfireIO_Hangfire.InMemory) [](https://sonarcloud.io/summary/new_code?id=HangfireIO_Hangfire.InMemory)
4 |
5 | This in-memory job storage aims to provide developers a quick way to start using Hangfire without additional infrastructure like SQL Server or Redis. It offers the flexibility to swap out the in-memory implementation in a production environment. This is not intended to compete with TPL or other in-memory processing libraries, as serialization itself incurs a significant overhead.
6 |
7 | The implementation includes proper synchronization mechanisms, utilizing blocking operations for locks, queues, and queries. This avoids active polling. Read and write operations are handled by a dedicated background thread to minimize synchronization between threads, keeping the system simple and ready for future async-based enhancements. Internal states use `SortedDictionary`, `SortedSet`, and `LinkedList` to avoid large allocations on the Large Object Heap, thus reducing potential `OutOfMemoryException` issues caused by memory fragmentation.
8 |
9 | Additionally, this storage uses a monotonic clock where possible, using the `Stopwatch.GetTimestamp` method. This ensures that expiration rules remain effective even if the system clock changes abruptly.
10 |
11 | ## Requirements
12 |
13 | * **Hangfire 1.8.0** and above: any latest version of Hangfire.InMemory
14 | * **Hangfire 1.7.0**: Hangfire.InMemory 0.11.0 and above
15 | * **Hangfire 1.6.0**: Hangfire.InMemory 0.3.7
16 |
17 | ## Installation
18 |
19 | [Hangfire.InMemory](https://www.nuget.org/packages/Hangfire.InMemory/) is available on NuGet. You can install it using your preferred package manager:
20 |
21 | ```powershell
22 | > dotnet add package Hangfire.InMemory
23 | ```
24 |
25 | ## Configuration
26 |
27 | After installing the package, configure it using the `UseInMemoryStorage` method on the `IGlobalConfiguration` interface:
28 |
29 | ```csharp
30 | GlobalConfiguration.Configuration.UseInMemoryStorage();
31 | ```
32 |
33 | ### Maximum Expiration Time
34 |
35 | Starting from version 0.7.0, the package controls the maximum expiration time for storage entries and sets it to *3 hours* by default when a higher expiration time is passed. For example, the default expiration time for background jobs is *24 hours*, and for batch jobs and their contents, the default time is *7 days*, which can be too big for in-memory storage that runs side-by-side with the application.
36 |
37 | You can control this behavior or even turn it off with the `MaxExpirationTime` option available in the `InMemoryStorageOptions` class in the following way:
38 |
39 | ```csharp
40 | GlobalConfiguration.Configuration.UseInMemoryStorage(new InMemoryStorageOptions
41 | {
42 | MaxExpirationTime = TimeSpan.FromHours(3) // Default value, we can also set it to `null` to disable.
43 | });
44 | ```
45 |
46 | It is also possible to use `TimeSpan.Zero` as a value for this option. In this case, entries will be removed immediately instead of relying on the time-based eviction implementation. Please note that some unwanted side effects may appear when using low value – for example, an antecedent background job may be created, processed, and expired before its continuation is created, resulting in exceptions.
47 |
48 | ### Comparing Keys
49 |
50 | Different storages use different rules for comparing keys. Some of them, like Redis, use case-sensitive comparisons, while others, like SQL Server, may use case-insensitive comparison implementation. It is possible to set this behavior explicitly and simplify moving to another storage implementation in a production environment by configuring the `StringComparer` option in the `InMemoryStorageOptions` class in the following way:
51 |
52 | ```csharp
53 | GlobalConfiguration.Configuration.UseInMemoryStorage(new InMemoryStorageOptions
54 | {
55 | StringComparer = StringComparer.Ordinal // Default value, case-sensitive.
56 | });
57 | ```
58 |
59 | ### Setting Key Type Jobs
60 |
61 | Starting from version 1.0, Hangfire.InMemory uses `long`-based keys for background jobs, similar to the Hangfire.SqlServer package. However, you can change this to use `Guid`-based keys to match the Hangfire.Pro.Redis experience. To do so, simply configure the `InMemoryStorageOptions.IdType` property as follows:
62 |
63 | ```csharp
64 | GlobalConfiguration.Configuration.UseInMemoryStorage(new InMemoryStorageOptions
65 | {
66 | IdType = InMemoryStorageIdType.Guid
67 | });
68 | ```
69 |
70 | ### Setting the Maximum State History Length
71 |
72 | The `MaxStateHistoryLength` option in the `InMemoryStorageOptions` class sets the maximum number of state history entries to be retained for each background job. This is useful for controlling memory usage by limiting the number of state transitions stored in memory.
73 |
74 | By default, Hangfire.InMemory retains `10` state history entries, but you can adjust this setting based on your application's requirements.
75 |
76 | ```csharp
77 | GlobalConfiguration.Configuration.UseInMemoryStorage(new InMemoryStorageOptions
78 | {
79 | MaxStateHistoryLength = 10 // default value
80 | });
81 | ```
--------------------------------------------------------------------------------
/appveyor.yml:
--------------------------------------------------------------------------------
1 | # AppVeyor CI build file, https://ci.appveyor.com/project/odinserj/hangfire
2 |
3 | # Notes:
4 | # - Minimal appveyor.yml file is an empty file. All sections are optional.
5 | # - Indent each level of configuration with 2 spaces. Do not use tabs!
6 | # - All section names are case-sensitive.
7 | # - Section names should be unique on each level.
8 |
9 | #---------------------------------#
10 | # environment configuration #
11 | #---------------------------------#
12 |
13 | # Please don't edit it manually, use the `build.bat version` command instead.
14 | version: 1.0.0-build-0{build}
15 |
16 | image:
17 | - Visual Studio 2022
18 | - Ubuntu2004
19 |
20 | environment:
21 | SIGNPATH_API_TOKEN:
22 | secure: gHJ9TRVbtow8s1pvgKnuOsHuZ9N8vye+513e60fqbvHmyyT3yzXQiL59T/x64/8k
23 |
24 | #---------------------------------#
25 | # build configuration #
26 | #---------------------------------#
27 |
28 | before_build:
29 | - pwsh: Install-PSResource -Name SignPath -TrustRepository
30 | - sh: nuget locals all -clear
31 |
32 | build_script:
33 | - cmd: build.bat sign
34 | - sh: dotnet test -c release -f netcoreapp3.1
35 | - sh: dotnet test -c release -f net6.0
36 |
37 | #---------------------------------#
38 | # tests configuration #
39 | #---------------------------------#
40 |
41 | test: off
42 |
43 | #---------------------------------#
44 | # artifacts configuration #
45 | #---------------------------------#
46 |
47 | artifacts:
48 | - path: 'build\**\*.nupkg'
49 | - path: 'build\**\*.zip'
50 |
51 | deploy:
52 | - provider: NuGet
53 | api_key:
54 | secure: hqCIcf//r7SvEBjm8DIHKko16YfrNJ1bfthMy/JPqKO/ov5qJmyPEBRnXisK26qV
55 | on:
56 | appveyor_repo_tag: true
57 |
--------------------------------------------------------------------------------
/build.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | .nuget\NuGet.exe restore .nuget\packages.config -OutputDirectory packages -UseLockFile -LockedMode -NoHttpCache || exit /b 666
3 | pwsh.exe -NoProfile -ExecutionPolicy RemoteSigned -Command "& {Import-Module '.\packages\psake.*\tools\psake.psm1'; invoke-psake .\psake-project.ps1 %*; if ($psake.build_success -eq $false) { exit 1 } else { exit 0 }; }"
4 | exit /B %errorlevel%
--------------------------------------------------------------------------------
/nuspecs/Hangfire.InMemory.nuspec:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Hangfire.InMemory
5 | %version%
6 | Hangfire In-Memory Storage
7 | Sergey Odinokov
8 | HangfireIO, odinserj
9 | https://github.com/HangfireIO/Hangfire.InMemory
10 |
11 | LICENSE.md
12 | icon.png
13 | README.md
14 | In-memory job storage for Hangfire with an efficient implementation.
15 | Copyright © 2020-2024 Hangfire OÜ
16 | Hangfire Storage In-Memory
17 | https://github.com/HangfireIO/Hangfire.InMemory/releases
18 | 1.0.0
19 | • Breaking – Remove the deprecated `DisableJobSerialization` option.
20 | • Breaking – Change default value for the `IdType` option to `long`.
21 |
22 | 0.10.4
23 | • Fixed – Problem with locks implementation due to a regression in .NET 8.0.
24 |
25 | 0.10.3
26 | • Changed – Significantly optimize `GetFirstByLowestScoreFromSet` method overloads.
27 |
28 | 0.10.2
29 | • Changed – Refactor command dispatching to make it more simple and less allocating.
30 | • Changed – Straightforward locking implementation with more unit tests.
31 | • Fixed – `InvalidOperationException` "Wrong level" when trying to release a lock (regression from 0.10.1).
32 | • Fixed – "An item with the same key has already been added" on the Awaiting Jobs page (regression from 0.10.0).
33 |
34 | 0.10.1
35 | • Changed – Roll back a breaking change in 0.10.0 for the `InMemoryStorageOptions` class.
36 | • Changed – Increase the default eviction interval to 5 seconds.
37 | • Changed – More efficient storage of state history records.
38 | • Changed – Implement fast path for the `FetchNextJob` method.
39 | • Fixed – More robust entry eviction implementation.
40 | • Fixed – Graceful dispatcher shutdown without additional waiting.
41 | • Project – Faster build pipeline on AppVeyor after migration to modern Powershell 7+.
42 |
43 | 0.10.0
44 | • Breaking – `InMemoryStorageOptions` class instances are now immutable after initialization.
45 | • Added – Support long-based job identifiers through the `InMemoryStorageOptions.IdType` property.
46 | • Added – Expose the `InMemoryStorageOptions.CommandTimeout` option to control the command timeouts.
47 | • Changed – Significantly improve query dispatching pipeline in terms of speed and allocations.
48 | • Changed – More compact representation of jobs and their parameters.
49 | • Changed – Optimise the `GetFirstByLowestScoreFromSet` query when the number of items is huge.
50 | • Changed – Better concurrency handling implementation for the collection of locks.
51 |
52 | 0.9.0
53 | • Added – Implement the disposable pattern for the `InMemoryStorage` class.
54 | • Changed – Use more compact representation of job parameters and state data.
55 | • Changed – Move to `SortedDictionary` and `LinkedList` to avoid using Large Object Heap.
56 | • Changed – `TimeSpan.Zero` value for `MaxExpirationTime` now causes immediate entry eviction.
57 | • Fixed – Ensure near-zero max expiration limit can't lead to uninitialized job eviction.
58 | • Deprecated – `DisableJobSerialization` option is now obsolete, serialization is always enabled.
59 |
60 | 0.8.1
61 | • Fixed – Incorrect validation in the `MaxStateHistoryLength` setter (by @DPschichholz).
62 |
63 | 0.8.0
64 | • Project – Sign NuGet package and .NET assemblies on build with a company's own certificate.
65 | • Project – Require package signature validation when restoring dependencies.
66 | • Project – Add HangfireIO as an owner for the NuGet package.
67 | • Project – Add readme file and icon to the NuGet package.
68 | • Project – Fix Git repository URL in the NuGet package metadata.
69 |
70 | 0.7.0
71 | • Added – `InMemoryStorageOptions.MaxExpirationTime` option to control the maximum expiration time.
72 | • Changed – The default value for maximum expiration time is 2 hours now, not days.
73 | • Fixed – Populate `ParametersSnapshot` and `InvocationData` properties in `IMonitoringApi.JobDetails`.
74 | • Fixed – The "Awaiting Jobs" page now includes the state name of an antecedent background job.
75 | • Fixed – The "Scheduled Jobs" page now has correct identifiers for jobs with explicit queues defined.
76 | • Fixed – Unify job ordering in Monitoring API to be the same as in other storages.
77 | • Project – Enable source link support with embedded symbols for simplified debugging.
78 | • Project – Refactored internals and added even more unit tests.
79 | • Project – Enable NuGet package restore with lock file and locked mode.
80 | • Project – Project and Release Notes URLs in the NuGet package now point to the repository.
81 | • Project – Enable tests running on the `net6.0` platform and Ubuntu on AppVeyor.
82 |
83 | 0.6.0
84 | • Added – `InMemoryStorageOptions.MaxStateHistoryLength` option to control state entries.
85 | • Changed – Always use monotonic clock when working with time.
86 | • Changed – Release distributed locks when their connection is disposed.
87 | • Changed – Pass dispatcher fault exceptions to a caller thread.
88 | • Project – Refactor internal types to have a cleaner project structure and avoid mistakes.
89 | • Project – Enable static analysis by the Microsoft.CodeAnalysis.NetAnalyzers package.
90 | • Project – Enable portable PDBs for the .NET Framework 4.5.1 platform.
91 |
92 | 0.5.1
93 | • Fixed – Infinite loop in recurring job scheduler consuming 100% CPU regression after 0.5.0.
94 |
95 | 0.5.0
96 | • Added – `InMemoryStorageOptions.StringComparer` as a central option for key and index comparisons.
97 |
98 | 0.4.1
99 | • Fixed – "Awaiting Jobs" metric is now correctly populated with `Version180` compatibility level.
100 |
101 | 0.4.0
102 | • Breaking – Package now depends on Hangfire.Core version 1.8.0.
103 | • Breaking – Replace the `net45` target with `net451` one as the former is not supported.
104 |
105 | • Changed – Improve `GetFirstByLowestScoreFromSet` operations.
106 | • Changed – Implement the `Job.Queue` feature.
107 | • Changed – Implement the `Connection.GetUtcDateTime` feature.
108 | • Changed – Implement the `Connection.GetSetContains` feature.
109 | • Changed – Implement the `Connection.GetSetCount.Limited` feature.
110 | • Changed – Implement the `Connection.BatchedGetFirstByLowestScoreFromSet` feature for the storage.
111 | • Changed – Implement the `Transaction.AcquireDistributedLock` feature.
112 | • Changed – Implement the `Transaction.CreateJob` feature.
113 | • Changed – Implement the `Transaction.SetJobParameter` feature.
114 | • Changed – Implement the new monitoring features.
115 | • Changed – Populate the new properties in Monitoring API.
116 | • Changed – Populate the `Retries` metric in the `GetStatistics` method.
117 |
118 | 0.3.7
119 | • Fixed – Throw `BackgroundJobServerGoneException` outside of dispatcher thread.
120 |
121 | 0.3.6
122 | • Fixed – Ensure lock entries are eventually removed from their collection.
123 | • Fixed – Ensure lock entries are always updated under a monitor.
124 |
125 | 0.3.5
126 | • Fixed – Ensure entries are expired even during constant storage pressure.
127 |
128 | 0.3.4
129 | • Fixed – Reverse state list instead of sorting it by date in the `JobDetails` method.
130 | • Fixed – Better sorting for state indexes, take into account job creation date.
131 | • Fixed – Reverse succeeded and deleted job lists to match Redis implementation.
132 |
133 | 0.3.3
134 | • Fixed – Sort queues and servers when returning them from monitoring api and in the Dashboard UI.
135 |
136 | 0.3.2
137 | • Fixed – Enqueued jobs may become invisible when adding a lot of jobs simultaneously to a new queue.
138 | • Fixed – Some workers are waiting for background jobs forever when several jobs added at once.
139 | • Fixed – Workers are able to detect new background jobs only after another background job is processed.
140 | • Fixed – Workers don't see background jobs when multiple queues are used with minimal workload.
141 |
142 | 0.3.1
143 | • Fixed – `NullReferenceException` in the `SignalOneQueueWaitNode` method when using multiple queues.
144 |
145 | 0.3.0
146 | • Added – `InMemoryStorageOptions.DisableJobSerialization` option.
147 | • Fixed – `ObjectDisposedException` on semaphore when committing a transaction.
148 | • Fixed – Gracefully handle `ObjectDisposedException` when signaling for query completion.
149 | • Fixed – Avoid killing the whole process in case of an exception in dispatcher, stop it instead.
150 | • Project – Add a lot of new unit tests for `InMemoryMonitoringApi` class.
151 |
152 | 0.2.0
153 | • Fixed – A lot of corner cases revealed by unit tests.
154 | • Project – Added a ton of unit tests for the top-level classes to ensure behavior is consistent.
155 |
156 | 0.1.0 – Initial release
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
--------------------------------------------------------------------------------
/nuspecs/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HangfireIO/Hangfire.InMemory/a68f39353edae4b23a4c0f5d61378ba8d1bb06ed/nuspecs/icon.png
--------------------------------------------------------------------------------
/psake-project.ps1:
--------------------------------------------------------------------------------
1 | Include "packages\Hangfire.Build.0.5.0\tools\psake-common.ps1"
2 |
3 | Task Default -Depends Pack
4 |
5 | Task Test -Depends Compile -Description "Run unit and integration tests." {
6 | Exec { dotnet test --no-build -c release "tests\Hangfire.InMemory.Tests" }
7 | }
8 |
9 | Task Collect -Depends Test -Description "Copy all artifacts to the build folder." {
10 | Collect-Assembly "Hangfire.InMemory" "net451"
11 | Collect-Assembly "Hangfire.InMemory" "netstandard2.0"
12 | Collect-File "LICENSE_ROYALTYFREE"
13 | Collect-File "LICENSE_STANDARD"
14 | Collect-File "COPYING.LESSER"
15 | Collect-File "COPYING"
16 | Collect-File "LICENSE.md"
17 | Collect-File "README.md"
18 | }
19 |
20 | Task Pack -Depends Collect -Description "Create NuGet packages and archive files." {
21 | $version = Get-PackageVersion
22 |
23 | Create-Package "Hangfire.InMemory" $version
24 | Create-Archive "Hangfire.InMemory-$version"
25 | }
26 |
27 | Task Sign -Depends Pack -Description "Sign artifacts." {
28 | $version = Get-PackageVersion
29 |
30 | Sign-ArchiveContents "Hangfire.InMemory-$version" "hangfire"
31 | }
32 |
--------------------------------------------------------------------------------
/samples/ConsoleSample/ConsoleSample.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/samples/ConsoleSample/HarnessHostedService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 | using Hangfire;
6 | using Microsoft.Extensions.Hosting;
7 | using Microsoft.Extensions.Logging;
8 |
9 | namespace ConsoleSample
10 | {
11 | public class HarnessHostedService : BackgroundService
12 | {
13 | private readonly IBackgroundJobClient _backgroundJobs;
14 | private readonly ILogger _logger;
15 |
16 | public HarnessHostedService(IBackgroundJobClient backgroundJobs, ILogger logger)
17 | {
18 | _backgroundJobs = backgroundJobs ?? throw new ArgumentNullException(nameof(backgroundJobs));
19 | _logger = logger ?? throw new ArgumentNullException(nameof(logger));
20 | }
21 |
22 | protected override Task ExecuteAsync(CancellationToken stoppingToken)
23 | {
24 | var sw = Stopwatch.StartNew();
25 |
26 | Parallel.For(0, 25_000, new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount }, i =>
27 | {
28 | _backgroundJobs.Enqueue("default" ,() => Empty());
29 | _backgroundJobs.Enqueue("critical", () => Empty());
30 | });
31 |
32 | _logger.LogInformation($"Enqueued in {sw.Elapsed}");
33 | return Task.CompletedTask;
34 | }
35 |
36 | public static void Empty()
37 | {
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/samples/ConsoleSample/Program.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Hosting;
2 | using Microsoft.Extensions.Hosting;
3 |
4 | namespace ConsoleSample
5 | {
6 | public class Program
7 | {
8 | public static void Main(string[] args)
9 | {
10 | CreateHostBuilder(args).Build().Run();
11 | }
12 |
13 | public static IHostBuilder CreateHostBuilder(string[] args) =>
14 | Host.CreateDefaultBuilder(args)
15 | .ConfigureWebHostDefaults(webBuilder =>
16 | {
17 | webBuilder.UseStartup();
18 | });
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/samples/ConsoleSample/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "iisSettings": {
3 | "windowsAuthentication": false,
4 | "anonymousAuthentication": true,
5 | "iisExpress": {
6 | "applicationUrl": "http://localhost:51728",
7 | "sslPort": 0
8 | }
9 | },
10 | "profiles": {
11 | "IIS Express": {
12 | "commandName": "IISExpress",
13 | "launchBrowser": true,
14 | "environmentVariables": {
15 | "ASPNETCORE_ENVIRONMENT": "Development"
16 | }
17 | },
18 | "ConsoleSample": {
19 | "commandName": "Project",
20 | "launchBrowser": true,
21 | "applicationUrl": "http://localhost:5000",
22 | "environmentVariables": {
23 | "ASPNETCORE_ENVIRONMENT": "Development"
24 | }
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/samples/ConsoleSample/Startup.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Hangfire;
3 | using Hangfire.InMemory;
4 | using Microsoft.AspNetCore.Builder;
5 | using Microsoft.AspNetCore.Hosting;
6 | using Microsoft.Extensions.DependencyInjection;
7 |
8 | namespace ConsoleSample
9 | {
10 | public class Startup
11 | {
12 | public void ConfigureServices(IServiceCollection services)
13 | {
14 | services.AddHangfire(config => config
15 | .SetDataCompatibilityLevel(CompatibilityLevel.Version_180)
16 | .UseSimpleAssemblyNameTypeSerializer()
17 | .UseIgnoredAssemblyVersionTypeResolver()
18 | .UseInMemoryStorage(new InMemoryStorageOptions
19 | {
20 | IdType = InMemoryStorageIdType.Long
21 | }));
22 |
23 | services.AddHangfireServer(options => options.Queues = new[] { "critical", "default" });
24 |
25 | services.AddHostedService();
26 | }
27 |
28 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
29 | {
30 | app.UseDeveloperExceptionPage();
31 | app.UseHangfireDashboard(String.Empty);
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/samples/ConsoleSample/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/samples/ConsoleSample/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | },
9 | "AllowedHosts": "*"
10 | }
11 |
--------------------------------------------------------------------------------
/samples/ConsoleSample/packages.lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 1,
3 | "dependencies": {
4 | "net6.0": {
5 | "Hangfire.AspNetCore": {
6 | "type": "Direct",
7 | "requested": "[1.8.15, )",
8 | "resolved": "1.8.15",
9 | "contentHash": "o85rtOYvhbWpNUGT4YrZE62lugShfdL3EMCqX2QoTC6eXVwpqtWOYfSeiTXAxlbF0CXNJJsuSCISKLmzbngWAA==",
10 | "dependencies": {
11 | "Hangfire.NetCore": "[1.8.15]"
12 | }
13 | },
14 | "Hangfire.Core": {
15 | "type": "Direct",
16 | "requested": "[1.8.15, )",
17 | "resolved": "1.8.15",
18 | "contentHash": "+w8gT6CFH4jicVEsJ8WlMRJMNV2MG52JNtvKoXPFHFs6nkDTND6iDeCjydyHgp+85lZPRXc+s9/vkxD2vbPrLg==",
19 | "dependencies": {
20 | "Newtonsoft.Json": "11.0.1"
21 | }
22 | },
23 | "Hangfire.NetCore": {
24 | "type": "Transitive",
25 | "resolved": "1.8.15",
26 | "contentHash": "HNACpklY1FGcsCr/xlPvmh5R5JqH2eEBxOp63Dwph6H6LdGWWqHoMpxjxkpYkZXM2mNpmk+j0Dk8lizadfnD+A==",
27 | "dependencies": {
28 | "Hangfire.Core": "[1.8.15]",
29 | "Microsoft.Extensions.DependencyInjection.Abstractions": "3.0.0",
30 | "Microsoft.Extensions.Hosting.Abstractions": "3.0.0",
31 | "Microsoft.Extensions.Logging.Abstractions": "3.0.0"
32 | }
33 | },
34 | "Microsoft.Extensions.Configuration.Abstractions": {
35 | "type": "Transitive",
36 | "resolved": "3.0.0",
37 | "contentHash": "Lge/PbXC53jI1MF2J92X5EZOeKV8Q/rlB1aV3H9I/ZTDyQGOyBcL03IAvnviWpHKj43BDkNy6kU2KKoh8kAS0g==",
38 | "dependencies": {
39 | "Microsoft.Extensions.Primitives": "3.0.0"
40 | }
41 | },
42 | "Microsoft.Extensions.DependencyInjection.Abstractions": {
43 | "type": "Transitive",
44 | "resolved": "3.0.0",
45 | "contentHash": "ofQRroDlzJ0xKOtzNuaVt6QKNImFkhkG0lIMpGl7PtXnIf5SuLWBeiQZAP8DNSxDBJJdcsPkiJiMYK2WA5H8dQ=="
46 | },
47 | "Microsoft.Extensions.FileProviders.Abstractions": {
48 | "type": "Transitive",
49 | "resolved": "3.0.0",
50 | "contentHash": "kahEeykb6FyQytoZNNXuz74X85B4weIEt8Kd+0klK48bkXDWOIHAOvNjlGsPMcS9CL935Te8QGQS83JqCbpdHA==",
51 | "dependencies": {
52 | "Microsoft.Extensions.Primitives": "3.0.0"
53 | }
54 | },
55 | "Microsoft.Extensions.Hosting.Abstractions": {
56 | "type": "Transitive",
57 | "resolved": "3.0.0",
58 | "contentHash": "qeDWS5ErmkUN96BdQqpmeCmLk5HJWQ/SPw3ux5v5/Qb0hKZS5wojBMulnBC7JUEiBwg7Ir71Yjf1lFiRT5MdtQ==",
59 | "dependencies": {
60 | "Microsoft.Extensions.Configuration.Abstractions": "3.0.0",
61 | "Microsoft.Extensions.DependencyInjection.Abstractions": "3.0.0",
62 | "Microsoft.Extensions.FileProviders.Abstractions": "3.0.0",
63 | "Microsoft.Extensions.Logging.Abstractions": "3.0.0"
64 | }
65 | },
66 | "Microsoft.Extensions.Logging.Abstractions": {
67 | "type": "Transitive",
68 | "resolved": "3.0.0",
69 | "contentHash": "+PsosTYZn+omucI0ff9eywo9QcPLwcbIWf7dz7ZLM1zGR8gVZXJ3wo6+tkuIedUNW5iWENlVJPEvrGjiVeoNNQ=="
70 | },
71 | "Microsoft.Extensions.Primitives": {
72 | "type": "Transitive",
73 | "resolved": "3.0.0",
74 | "contentHash": "6gwewTbmOh+ZVBicVkL1XRp79sx4O7BVY6Yy+7OYZdwn3pyOKe9lOam+3gXJ3TZMjhJZdV0Ub8hxHt2vkrmN5Q=="
75 | },
76 | "Newtonsoft.Json": {
77 | "type": "Transitive",
78 | "resolved": "11.0.1",
79 | "contentHash": "pNN4l+J6LlpIvHOeNdXlwxv39NPJ2B5klz+Rd2UQZIx30Squ5oND1Yy3wEAUoKn0GPUj6Yxt9lxlYWQqfZcvKg=="
80 | },
81 | "hangfire.inmemory": {
82 | "type": "Project",
83 | "dependencies": {
84 | "Hangfire.Core": "[1.8.0, )"
85 | }
86 | }
87 | }
88 | }
89 | }
--------------------------------------------------------------------------------
/src/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | false
6 | true
7 | embedded
8 | true
9 |
10 |
11 |
12 | true
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | true
27 | latest
28 | All
29 | true
30 |
31 |
--------------------------------------------------------------------------------
/src/Hangfire.InMemory/Entities/CounterEntry.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.InMemory. Copyright © 2020 Hangfire OÜ.
2 | //
3 | // Hangfire is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU Lesser General Public License as
5 | // published by the Free Software Foundation, either version 3
6 | // of the License, or any later version.
7 | //
8 | // Hangfire is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU Lesser General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU Lesser General Public
14 | // License along with Hangfire. If not, see .
15 |
16 | using Hangfire.InMemory.State;
17 |
18 | namespace Hangfire.InMemory.Entities
19 | {
20 | internal sealed class CounterEntry : IExpirableEntry
21 | {
22 | public CounterEntry(string id)
23 | {
24 | Key = id;
25 | }
26 |
27 | public string Key { get; }
28 | public long Value { get; set; }
29 | public MonotonicTime? ExpireAt { get; set; }
30 | }
31 | }
--------------------------------------------------------------------------------
/src/Hangfire.InMemory/Entities/ExpirableEntryComparer.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.InMemory. Copyright © 2020 Hangfire OÜ.
2 | //
3 | // Hangfire is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU Lesser General Public License as
5 | // published by the Free Software Foundation, either version 3
6 | // of the License, or any later version.
7 | //
8 | // Hangfire is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU Lesser General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU Lesser General Public
14 | // License along with Hangfire. If not, see .
15 |
16 | using System;
17 | using System.Collections.Generic;
18 |
19 | namespace Hangfire.InMemory.Entities
20 | {
21 | internal sealed class ExpirableEntryComparer : IComparer>
22 | where T : IComparable
23 | {
24 | private readonly IComparer? _comparer;
25 |
26 | public ExpirableEntryComparer(IComparer? comparer)
27 | {
28 | _comparer = comparer;
29 | }
30 |
31 | public int Compare(IExpirableEntry? x, IExpirableEntry? y)
32 | {
33 | if (ReferenceEquals(x, y)) return 0;
34 |
35 | // Place nulls last just in case, because they will prevent expiration
36 | // manager from correctly running and stopping earlier, since it works
37 | // from first value until is higher than the current time.
38 | if (x == null) return +1;
39 | if (y == null) return -1;
40 |
41 | if (x.ExpireAt.HasValue && y.ExpireAt.HasValue)
42 | {
43 | var expirationCompare = x.ExpireAt.Value.CompareTo(y.ExpireAt.Value);
44 | if (expirationCompare != 0) return expirationCompare;
45 | }
46 | else if (!x.ExpireAt.HasValue && y.ExpireAt.HasValue)
47 | {
48 | return +1;
49 | }
50 | else if (!y.ExpireAt.HasValue && x.ExpireAt.HasValue)
51 | {
52 | return -1;
53 | }
54 |
55 | return _comparer?.Compare(x.Key, y.Key) ?? x.Key.CompareTo(y.Key);
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/Hangfire.InMemory/Entities/HashEntry.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.InMemory. Copyright © 2020 Hangfire OÜ.
2 | //
3 | // Hangfire is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU Lesser General Public License as
5 | // published by the Free Software Foundation, either version 3
6 | // of the License, or any later version.
7 | //
8 | // Hangfire is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU Lesser General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU Lesser General Public
14 | // License along with Hangfire. If not, see .
15 |
16 | using System;
17 | using System.Collections.Generic;
18 | using Hangfire.InMemory.State;
19 |
20 | namespace Hangfire.InMemory.Entities
21 | {
22 | internal sealed class HashEntry : IExpirableEntry
23 | {
24 | public HashEntry(string id, StringComparer comparer)
25 | {
26 | Key = id;
27 | Value = new SortedDictionary(comparer);
28 | }
29 |
30 | public string Key { get; }
31 | public IDictionary Value { get; }
32 | public MonotonicTime? ExpireAt { get; set; }
33 | }
34 | }
--------------------------------------------------------------------------------
/src/Hangfire.InMemory/Entities/IExpirableEntry.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.InMemory. Copyright © 2020 Hangfire OÜ.
2 | //
3 | // Hangfire is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU Lesser General Public License as
5 | // published by the Free Software Foundation, either version 3
6 | // of the License, or any later version.
7 | //
8 | // Hangfire is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU Lesser General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU Lesser General Public
14 | // License along with Hangfire. If not, see .
15 |
16 | using Hangfire.InMemory.State;
17 |
18 | namespace Hangfire.InMemory.Entities
19 | {
20 | internal interface IExpirableEntry
21 | {
22 | T Key { get; }
23 | MonotonicTime? ExpireAt { get; set; }
24 | }
25 | }
--------------------------------------------------------------------------------
/src/Hangfire.InMemory/Entities/JobEntry.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.InMemory. Copyright © 2020 Hangfire OÜ.
2 | //
3 | // Hangfire is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU Lesser General Public License as
5 | // published by the Free Software Foundation, either version 3
6 | // of the License, or any later version.
7 | //
8 | // Hangfire is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU Lesser General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU Lesser General Public
14 | // License along with Hangfire. If not, see .
15 |
16 | using System;
17 | using System.Collections.Generic;
18 | using Hangfire.InMemory.State;
19 | using Hangfire.Storage;
20 |
21 | namespace Hangfire.InMemory.Entities
22 | {
23 | internal sealed class JobEntry : IExpirableEntry
24 | {
25 | private StateRecord[] _history = [];
26 | private KeyValuePair[] _parameters;
27 |
28 | public JobEntry(
29 | T key,
30 | InvocationData data,
31 | KeyValuePair[] parameters,
32 | MonotonicTime createdAt)
33 | {
34 | Key = key;
35 | InvocationData = data;
36 | CreatedAt = createdAt;
37 |
38 | _parameters = parameters;
39 | }
40 |
41 | public T Key { get; }
42 | public InvocationData InvocationData { get; internal set; }
43 |
44 | public StateRecord? State { get; set; }
45 | public IEnumerable History => _history;
46 | public MonotonicTime CreatedAt { get; }
47 | public MonotonicTime? ExpireAt { get; set; }
48 |
49 | public string? GetParameter(string name, StringComparer comparer)
50 | {
51 | foreach (var parameter in _parameters)
52 | {
53 | if (comparer.Compare(parameter.Key, name) == 0)
54 | {
55 | return parameter.Value;
56 | }
57 | }
58 |
59 | return null;
60 | }
61 |
62 | public void SetParameter(string name, string? value, StringComparer comparer)
63 | {
64 | var parameter = new KeyValuePair(name, value);
65 |
66 | for (var i = 0; i < _parameters.Length; i++)
67 | {
68 | if (comparer.Compare(_parameters[i].Key, name) == 0)
69 | {
70 | _parameters[i] = parameter;
71 | return;
72 | }
73 | }
74 |
75 | Array.Resize(ref _parameters, _parameters.Length + 1);
76 | _parameters[_parameters.Length - 1] = parameter;
77 | }
78 |
79 | public KeyValuePair[] GetParameters()
80 | {
81 | return _parameters;
82 | }
83 |
84 | public void AddHistoryEntry(StateRecord record, int maxLength)
85 | {
86 | if (record == null) throw new ArgumentNullException(nameof(record));
87 | if (maxLength <= 0) throw new ArgumentOutOfRangeException(nameof(maxLength));
88 |
89 | if (_history.Length < maxLength)
90 | {
91 | Array.Resize(ref _history, _history.Length + 1);
92 | }
93 | else
94 | {
95 | Array.Copy(_history, 1, _history, 0, _history.Length - 1);
96 | }
97 |
98 | _history[_history.Length - 1] = record;
99 | }
100 | }
101 | }
--------------------------------------------------------------------------------
/src/Hangfire.InMemory/Entities/JobStateCreatedAtComparer.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.InMemory. Copyright © 2020 Hangfire OÜ.
2 | //
3 | // Hangfire is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU Lesser General Public License as
5 | // published by the Free Software Foundation, either version 3
6 | // of the License, or any later version.
7 | //
8 | // Hangfire is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU Lesser General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU Lesser General Public
14 | // License along with Hangfire. If not, see .
15 |
16 | using System;
17 | using System.Collections.Generic;
18 |
19 | namespace Hangfire.InMemory.Entities
20 | {
21 | internal sealed class JobStateCreatedAtComparer : IComparer>
22 | where T : IComparable
23 | {
24 | private readonly IComparer? _comparer;
25 |
26 | public JobStateCreatedAtComparer(IComparer? comparer)
27 | {
28 | _comparer = comparer;
29 | }
30 |
31 | public int Compare(JobEntry? x, JobEntry? y)
32 | {
33 | if (ReferenceEquals(x, y)) return 0;
34 | if (x == null) return -1;
35 | if (y == null) return 1;
36 |
37 | if (ReferenceEquals(x.State, y.State)) return 0;
38 | if (x.State == null) return -1;
39 | if (y.State == null) return 1;
40 |
41 | var stateCreatedAtComparison = x.State.CreatedAt.CompareTo(y.State.CreatedAt);
42 | if (stateCreatedAtComparison != 0) return stateCreatedAtComparison;
43 |
44 | var createdAtComparison = x.CreatedAt.CompareTo(y.CreatedAt);
45 | if (createdAtComparison != 0) return createdAtComparison;
46 |
47 | return _comparer?.Compare(x.Key, y.Key) ?? x.Key.CompareTo(y.Key);
48 | }
49 | }
50 | }
--------------------------------------------------------------------------------
/src/Hangfire.InMemory/Entities/ListEntry.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.InMemory. Copyright © 2020 Hangfire OÜ.
2 | //
3 | // Hangfire is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU Lesser General Public License as
5 | // published by the Free Software Foundation, either version 3
6 | // of the License, or any later version.
7 | //
8 | // Hangfire is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU Lesser General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU Lesser General Public
14 | // License along with Hangfire. If not, see .
15 |
16 | using System;
17 | using System.Collections;
18 | using System.Collections.Generic;
19 | using Hangfire.InMemory.State;
20 |
21 | namespace Hangfire.InMemory.Entities
22 | {
23 | internal sealed class ListEntry : IExpirableEntry, IEnumerable
24 | {
25 | private readonly LinkedList _list = new LinkedList();
26 |
27 | public ListEntry(string id)
28 | {
29 | Key = id;
30 | }
31 |
32 | public string Key { get; }
33 | public MonotonicTime? ExpireAt { get; set; }
34 |
35 | public int Count => _list.Count;
36 |
37 | public void Add(string value)
38 | {
39 | _list.AddFirst(value);
40 | }
41 |
42 | public int RemoveAll(string value, StringComparer comparer)
43 | {
44 | var node = _list.First;
45 | while (node != null)
46 | {
47 | var current = node;
48 | node = node.Next;
49 |
50 | if (comparer.Compare(current.Value, value) == 0)
51 | {
52 | _list.Remove(current);
53 | }
54 | }
55 |
56 | return _list.Count;
57 | }
58 |
59 | public int Trim(int keepStartingFrom, int keepEndingAt)
60 | {
61 | var count = keepEndingAt - keepStartingFrom + 1;
62 |
63 | var node = _list.First;
64 |
65 | // Removing first items
66 | while (node != null && keepStartingFrom-- > 0)
67 | {
68 | var current = node;
69 | node = node.Next;
70 |
71 | _list.Remove(current);
72 | }
73 |
74 | if (node != null)
75 | {
76 | // Skipping required entries
77 | while (node != null && count-- > 0)
78 | {
79 | node = node.Next;
80 | }
81 |
82 | // Removing rest items
83 | while (node != null)
84 | {
85 | var current = node;
86 | node = node.Next;
87 |
88 | _list.Remove(current);
89 | }
90 | }
91 |
92 | return _list.Count;
93 | }
94 |
95 | public IEnumerator GetEnumerator()
96 | {
97 | return _list.GetEnumerator();
98 | }
99 |
100 | IEnumerator IEnumerable.GetEnumerator()
101 | {
102 | return GetEnumerator();
103 | }
104 | }
105 | }
--------------------------------------------------------------------------------
/src/Hangfire.InMemory/Entities/LockEntry.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.InMemory. Copyright © 2020 Hangfire OÜ.
2 | //
3 | // Hangfire is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU Lesser General Public License as
5 | // published by the Free Software Foundation, either version 3
6 | // of the License, or any later version.
7 | //
8 | // Hangfire is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU Lesser General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU Lesser General Public
14 | // License along with Hangfire. If not, see .
15 |
16 | using System;
17 | using System.Threading;
18 |
19 | namespace Hangfire.InMemory.Entities
20 | {
21 | internal sealed class LockEntry : IDisposable where T : class
22 | {
23 | private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(initialCount: 1, maxCount: 1);
24 | private T? _owner;
25 | private int _referenceCount;
26 | private int _level;
27 | private bool _finalized;
28 |
29 | public bool TryAcquire(T owner, TimeSpan timeout, out bool retry, out bool cleanUp)
30 | {
31 | if (owner == null) throw new ArgumentNullException(nameof(owner));
32 |
33 | retry = false;
34 | cleanUp = false;
35 |
36 | lock (_semaphore)
37 | {
38 | if (_finalized)
39 | {
40 | // Our entry was finalized by someone else, so we should retry
41 | // with a completely new entry.
42 | retry = true;
43 | return false;
44 | }
45 |
46 | if (ReferenceEquals(_owner, owner))
47 | {
48 | // Entry is currently owned by the same owner, so our lock has been
49 | // already acquired.
50 | _level++;
51 | return true;
52 | }
53 |
54 | // Whether it's already owned or not, we should increase
55 | // the number of references to avoid finalizing it too early and
56 | // allow waiting for it.
57 | _referenceCount++;
58 | }
59 |
60 | var waitResult = _semaphore.Wait(timeout);
61 |
62 | lock (_semaphore)
63 | {
64 | if (!waitResult)
65 | {
66 | _referenceCount--;
67 |
68 | // Finalize if there are no other references and request to clean up
69 | // in this case. No retry is needed, just give up.
70 | cleanUp = _finalized = _referenceCount == 0;
71 | return false;
72 | }
73 |
74 | _owner = owner;
75 | _level = 1;
76 | return true;
77 | }
78 | }
79 |
80 | public void Release(T owner, out bool cleanUp)
81 | {
82 | if (owner == null) throw new ArgumentNullException(nameof(owner));
83 |
84 | cleanUp = false;
85 | var release = false;
86 |
87 | lock (_semaphore)
88 | {
89 | if (_finalized) ThrowFinalizedException();
90 | if (!ReferenceEquals(_owner, owner)) throw new ArgumentException("Wrong entry owner", nameof(owner));
91 | if (_level <= 0) throw new InvalidOperationException("Wrong level");
92 | if (_referenceCount <= 0) throw new InvalidOperationException("Wrong reference count");
93 |
94 | _level--;
95 |
96 | if (_level == 0)
97 | {
98 | _owner = null;
99 | release = true;
100 | }
101 | }
102 |
103 | if (release)
104 | {
105 | _semaphore.Release();
106 |
107 | lock (_semaphore)
108 | {
109 | _referenceCount--;
110 | cleanUp = _finalized = _referenceCount == 0;
111 | }
112 | }
113 | }
114 |
115 | public void Dispose()
116 | {
117 | _semaphore.Dispose();
118 | }
119 |
120 | private static void ThrowFinalizedException()
121 | {
122 | throw new InvalidOperationException("Lock entry is already finalized.");
123 | }
124 | }
125 | }
--------------------------------------------------------------------------------
/src/Hangfire.InMemory/Entities/QueueEntry.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.InMemory. Copyright © 2020 Hangfire OÜ.
2 | //
3 | // Hangfire is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU Lesser General Public License as
5 | // published by the Free Software Foundation, either version 3
6 | // of the License, or any later version.
7 | //
8 | // Hangfire is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU Lesser General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU Lesser General Public
14 | // License along with Hangfire. If not, see .
15 |
16 | using System;
17 | using System.Collections.Concurrent;
18 | using System.Threading;
19 |
20 | namespace Hangfire.InMemory.Entities
21 | {
22 | internal sealed class QueueEntry
23 | where TKey : IComparable
24 | {
25 | private static readonly QueueWaitNode Tombstone = new QueueWaitNode(null);
26 |
27 | public ConcurrentQueue Queue { get; } = new ConcurrentQueue();
28 | public QueueWaitNode WaitHead { get; } = new QueueWaitNode(null);
29 |
30 | public void AddWaitNode(QueueWaitNode node)
31 | {
32 | if (node == null) throw new ArgumentNullException(nameof(node));
33 |
34 | var headNext = node.Next = null;
35 | var spinWait = new SpinWait();
36 |
37 | while (true)
38 | {
39 | var newNext = Interlocked.CompareExchange(ref WaitHead.Next, node, headNext);
40 | if (newNext == headNext) break;
41 |
42 | headNext = node.Next = newNext;
43 | spinWait.SpinOnce();
44 | }
45 | }
46 |
47 | public void SignalOneWaitNode()
48 | {
49 | if (Volatile.Read(ref WaitHead.Next) == null) return;
50 | SignalOneWaitNodeSlow();
51 | }
52 |
53 | private void SignalOneWaitNodeSlow()
54 | {
55 | while (true)
56 | {
57 | var node = Interlocked.Exchange(ref WaitHead.Next, null);
58 | if (node == null) return;
59 |
60 | var tailNode = Interlocked.Exchange(ref node.Next, Tombstone);
61 | if (tailNode != null)
62 | {
63 | var waitHead = WaitHead;
64 | do
65 | {
66 | waitHead = Interlocked.CompareExchange(ref waitHead.Next, tailNode, null);
67 | if (ReferenceEquals(waitHead, Tombstone))
68 | {
69 | waitHead = WaitHead;
70 | }
71 | } while (waitHead != null);
72 | }
73 |
74 | try
75 | {
76 | if (node.Value == null)
77 | {
78 | throw new InvalidOperationException("Trying to signal on a Tombstone object.");
79 | }
80 |
81 | node.Value.Set();
82 | return;
83 | }
84 | catch (ObjectDisposedException)
85 | {
86 | // Benign race condition, nothing to signal in this case.
87 | }
88 | }
89 | }
90 | }
91 | }
--------------------------------------------------------------------------------
/src/Hangfire.InMemory/Entities/QueueWaitNode.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.InMemory. Copyright © 2020 Hangfire OÜ.
2 | //
3 | // Hangfire is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU Lesser General Public License as
5 | // published by the Free Software Foundation, either version 3
6 | // of the License, or any later version.
7 | //
8 | // Hangfire is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU Lesser General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU Lesser General Public
14 | // License along with Hangfire. If not, see .
15 |
16 | using System.Threading;
17 |
18 | namespace Hangfire.InMemory.Entities
19 | {
20 | internal sealed class QueueWaitNode
21 | {
22 | public QueueWaitNode(AutoResetEvent? value)
23 | {
24 | Value = value;
25 | }
26 |
27 | public readonly AutoResetEvent? Value;
28 | public QueueWaitNode? Next;
29 | }
30 | }
--------------------------------------------------------------------------------
/src/Hangfire.InMemory/Entities/ServerEntry.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.InMemory. Copyright © 2020 Hangfire OÜ.
2 | //
3 | // Hangfire is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU Lesser General Public License as
5 | // published by the Free Software Foundation, either version 3
6 | // of the License, or any later version.
7 | //
8 | // Hangfire is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU Lesser General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU Lesser General Public
14 | // License along with Hangfire. If not, see .
15 |
16 | using Hangfire.InMemory.State;
17 | using Hangfire.Server;
18 |
19 | namespace Hangfire.InMemory.Entities
20 | {
21 | internal sealed class ServerEntry
22 | {
23 | public ServerEntry(ServerContext context, MonotonicTime startedAt)
24 | {
25 | Context = context;
26 | StartedAt = startedAt;
27 | HeartbeatAt = startedAt;
28 | }
29 |
30 | public ServerContext Context { get; }
31 | public MonotonicTime StartedAt { get; }
32 | public MonotonicTime HeartbeatAt { get; set; }
33 | }
34 | }
--------------------------------------------------------------------------------
/src/Hangfire.InMemory/Entities/SetEntry.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.InMemory. Copyright © 2020 Hangfire OÜ.
2 | //
3 | // Hangfire is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU Lesser General Public License as
5 | // published by the Free Software Foundation, either version 3
6 | // of the License, or any later version.
7 | //
8 | // Hangfire is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU Lesser General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU Lesser General Public
14 | // License along with Hangfire. If not, see .
15 |
16 | using System;
17 | using System.Collections;
18 | using System.Collections.Generic;
19 | using Hangfire.InMemory.State;
20 |
21 | namespace Hangfire.InMemory.Entities
22 | {
23 | internal sealed class SetEntry : IExpirableEntry, IEnumerable
24 | {
25 | private readonly SortedDictionary _hash;
26 | private readonly SortedSet _value;
27 |
28 | public SetEntry(string id, StringComparer stringComparer)
29 | {
30 | _hash = new SortedDictionary(stringComparer);
31 | _value = new SortedSet(new SortedSetItemComparer(stringComparer));
32 | Key = id;
33 | }
34 |
35 | public string Key { get; }
36 | public MonotonicTime? ExpireAt { get; set; }
37 |
38 | public int Count => _value.Count;
39 |
40 | public void Add(string value, double score)
41 | {
42 | if (!_hash.TryGetValue(value, out var entry))
43 | {
44 | entry = new SortedSetItem(value, score);
45 | _value.Add(entry);
46 | _hash.Add(value, entry);
47 | }
48 | else
49 | {
50 | // Element already exists, just need to add a score value – re-create it.
51 | _value.Remove(entry);
52 |
53 | entry = new SortedSetItem(value, score);
54 | _value.Add(entry);
55 | _hash[value] = entry;
56 | }
57 | }
58 |
59 | public List GetViewBetween(double from, double to, int count)
60 | {
61 | if (_value.Count == 0) return new List();
62 |
63 | var result = new List(count);
64 |
65 | if (_value.Min.Score >= from)
66 | {
67 | // Fast path - item is found, no need to traverse the tree, just iterating
68 | foreach (var item in _value)
69 | {
70 | if (item.Score > to || count-- == 0) break;
71 | result.Add(item.Value);
72 | }
73 | }
74 | else
75 | {
76 | // Slow path - find the item first
77 | var view = _value.GetViewBetween(
78 | new SortedSetItem(null!, from),
79 | new SortedSetItem(null!, to));
80 |
81 | // Don't query view.Count here as it leads to VersionCheck(updateCount: true) call,
82 | // which is very expensive when there are a huge number of entries.
83 | foreach (var item in view)
84 | {
85 | if (count-- == 0) break;
86 | result.Add(item.Value);
87 | }
88 | }
89 |
90 | return result;
91 | }
92 |
93 | public string? GetFirstBetween(double from, double to)
94 | {
95 | if (_value.Count == 0) return null;
96 |
97 | var minItem = _value.Min;
98 | if (minItem.Score >= from)
99 | {
100 | // Fast path - item is found, no need to traverse
101 | return minItem.Score <= to ? minItem.Value : null;
102 | }
103 |
104 | // Slow path - find the item first
105 | var view = _value.GetViewBetween(
106 | new SortedSetItem(null!, from),
107 | new SortedSetItem(null!, to));
108 |
109 | return view.Min.Value;
110 | }
111 |
112 | public void Remove(string value)
113 | {
114 | if (_hash.TryGetValue(value, out var entry))
115 | {
116 | _value.Remove(entry);
117 | _hash.Remove(value);
118 | }
119 | }
120 |
121 | public bool Contains(string value)
122 | {
123 | return _hash.ContainsKey(value);
124 | }
125 |
126 | public IEnumerator GetEnumerator()
127 | {
128 | return _value.GetEnumerator();
129 | }
130 |
131 | IEnumerator IEnumerable.GetEnumerator()
132 | {
133 | return GetEnumerator();
134 | }
135 | }
136 | }
--------------------------------------------------------------------------------
/src/Hangfire.InMemory/Entities/SortedSetItem.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.InMemory. Copyright © 2020 Hangfire OÜ.
2 | //
3 | // Hangfire is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU Lesser General Public License as
5 | // published by the Free Software Foundation, either version 3
6 | // of the License, or any later version.
7 | //
8 | // Hangfire is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU Lesser General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU Lesser General Public
14 | // License along with Hangfire. If not, see .
15 |
16 | using System;
17 |
18 | namespace Hangfire.InMemory.Entities
19 | {
20 | internal readonly struct SortedSetItem : IEquatable
21 | {
22 | public SortedSetItem(string value, double score)
23 | {
24 | Value = value;
25 | Score = score;
26 | }
27 |
28 | public string Value { get; }
29 | public double Score { get; }
30 |
31 | public bool Equals(SortedSetItem other)
32 | {
33 | return Value == other.Value && Score.Equals(other.Score);
34 | }
35 |
36 | public override bool Equals(object? obj)
37 | {
38 | return obj is SortedSetItem other && Equals(other);
39 | }
40 |
41 | public override int GetHashCode()
42 | {
43 | unchecked
44 | {
45 | return ((Value != null ? Value.GetHashCode() : 0) * 397) ^ Score.GetHashCode();
46 | }
47 | }
48 |
49 | public override string ToString()
50 | {
51 | return $"Value: {Value}, Score: {Score}]";
52 | }
53 | }
54 | }
--------------------------------------------------------------------------------
/src/Hangfire.InMemory/Entities/SortedSetItemComparer.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.InMemory. Copyright © 2020 Hangfire OÜ.
2 | //
3 | // Hangfire is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU Lesser General Public License as
5 | // published by the Free Software Foundation, either version 3
6 | // of the License, or any later version.
7 | //
8 | // Hangfire is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU Lesser General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU Lesser General Public
14 | // License along with Hangfire. If not, see .
15 |
16 | using System;
17 | using System.Collections.Generic;
18 |
19 | namespace Hangfire.InMemory.Entities
20 | {
21 | internal sealed class SortedSetItemComparer : IComparer
22 | {
23 | private readonly StringComparer _stringComparer;
24 |
25 | public SortedSetItemComparer(StringComparer stringComparer)
26 | {
27 | _stringComparer = stringComparer;
28 | }
29 |
30 | public int Compare(SortedSetItem x, SortedSetItem y)
31 | {
32 | var scoreComparison = x.Score.CompareTo(y.Score);
33 | if (scoreComparison != 0 ||
34 | ReferenceEquals(null, y.Value) ||
35 | ReferenceEquals(null, x.Value))
36 | {
37 | return scoreComparison;
38 | }
39 |
40 | return _stringComparer.Compare(x.Value, y.Value);
41 | }
42 | }
43 | }
--------------------------------------------------------------------------------
/src/Hangfire.InMemory/Entities/StateRecord.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.InMemory. Copyright © 2020 Hangfire OÜ.
2 | //
3 | // Hangfire is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU Lesser General Public License as
5 | // published by the Free Software Foundation, either version 3
6 | // of the License, or any later version.
7 | //
8 | // Hangfire is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU Lesser General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU Lesser General Public
14 | // License along with Hangfire. If not, see .
15 |
16 | using System.Collections.Generic;
17 | using Hangfire.InMemory.State;
18 |
19 | namespace Hangfire.InMemory.Entities
20 | {
21 | internal sealed class StateRecord
22 | {
23 | public StateRecord(string name, string? reason, KeyValuePair[] data, MonotonicTime createdAt)
24 | {
25 | Name = name;
26 | Reason = reason;
27 | Data = data;
28 | CreatedAt = createdAt;
29 | }
30 |
31 | public string Name { get; }
32 | public string? Reason { get; }
33 | public MonotonicTime CreatedAt { get; }
34 | public KeyValuePair[] Data { get; }
35 | }
36 | }
--------------------------------------------------------------------------------
/src/Hangfire.InMemory/GlobalConfigurationExtensions.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.InMemory. Copyright © 2020 Hangfire OÜ.
2 | //
3 | // Hangfire is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU Lesser General Public License as
5 | // published by the Free Software Foundation, either version 3
6 | // of the License, or any later version.
7 | //
8 | // Hangfire is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU Lesser General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU Lesser General Public
14 | // License along with Hangfire. If not, see .
15 |
16 | using System;
17 | using System.ComponentModel;
18 | using System.Diagnostics.CodeAnalysis;
19 | using Hangfire.Annotations;
20 | using Hangfire.InMemory;
21 |
22 | // ReSharper disable once CheckNamespace
23 | namespace Hangfire
24 | {
25 | ///
26 | /// Provides extension methods for global configuration to use .
27 | ///
28 | [EditorBrowsable(EditorBrowsableState.Never)]
29 | [SuppressMessage("ReSharper", "RedundantNullnessAttributeWithNullableReferenceTypes", Justification = "Should be used for public classes")]
30 | public static class GlobalConfigurationExtensions
31 | {
32 | ///
33 | /// Configures Hangfire to use the with default options.
34 | ///
35 | /// The global configuration on which to set the in-memory storage.
36 | /// An instance of for chaining further configuration.
37 | /// Thrown when the argument is null.
38 | [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "Created in a static scope")]
39 | public static IGlobalConfiguration UseInMemoryStorage(
40 | [NotNull] this IGlobalConfiguration configuration)
41 | {
42 | if (configuration == null) throw new ArgumentNullException(nameof(configuration));
43 | return configuration.UseStorage(new InMemoryStorage());
44 | }
45 |
46 | ///
47 | /// Configures Hangfire to use the with the specified options.
48 | ///
49 | /// The global configuration on which to set the in-memory storage.
50 | /// Options for the in-memory storage.
51 | /// An instance of for chaining further configuration.
52 | /// Thrown when the or argument is null.
53 | [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "Created in a static scope")]
54 | public static IGlobalConfiguration UseInMemoryStorage(
55 | [NotNull] this IGlobalConfiguration configuration,
56 | [NotNull] InMemoryStorageOptions options)
57 | {
58 | if (configuration == null) throw new ArgumentNullException(nameof(configuration));
59 | if (options == null) throw new ArgumentNullException(nameof(options));
60 |
61 | return configuration.UseStorage(new InMemoryStorage(options));
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/Hangfire.InMemory/Hangfire.InMemory.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net451;netstandard2.0
5 | Latest
6 | enable
7 | $(DefineConstants);HANGFIRE_180
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/Hangfire.InMemory/IKeyProvider.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.InMemory. Copyright © 2024 Hangfire OÜ.
2 | //
3 | // Hangfire is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU Lesser General Public License as
5 | // published by the Free Software Foundation, either version 3
6 | // of the License, or any later version.
7 | //
8 | // Hangfire is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU Lesser General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU Lesser General Public
14 | // License along with Hangfire. If not, see .
15 |
16 | namespace Hangfire.InMemory
17 | {
18 | internal interface IKeyProvider
19 | {
20 | T GetUniqueKey();
21 | bool TryParse(string input, out T key);
22 | string ToString(T key);
23 | }
24 | }
--------------------------------------------------------------------------------
/src/Hangfire.InMemory/InMemoryFetchedJob.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.InMemory. Copyright © 2020 Hangfire OÜ.
2 | //
3 | // Hangfire is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU Lesser General Public License as
5 | // published by the Free Software Foundation, either version 3
6 | // of the License, or any later version.
7 | //
8 | // Hangfire is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU Lesser General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU Lesser General Public
14 | // License along with Hangfire. If not, see .
15 |
16 | using System;
17 | using Hangfire.InMemory.State;
18 | using Hangfire.Storage;
19 |
20 | namespace Hangfire.InMemory
21 | {
22 | internal sealed class InMemoryFetchedJob : IFetchedJob
23 | where TKey : IComparable
24 | {
25 | private readonly InMemoryConnection _connection;
26 |
27 | public InMemoryFetchedJob(
28 | InMemoryConnection connection,
29 | string queueName,
30 | string jobId)
31 | {
32 | _connection = connection ?? throw new ArgumentNullException(nameof(connection));
33 |
34 | QueueName = queueName ?? throw new ArgumentNullException(nameof(queueName));
35 | JobId = jobId ?? throw new ArgumentNullException(nameof(jobId));
36 | }
37 |
38 | public string QueueName { get; }
39 | public string JobId { get; }
40 |
41 | public void Requeue()
42 | {
43 | if (!_connection.KeyProvider.TryParse(JobId, out var key))
44 | {
45 | return;
46 | }
47 |
48 | var entry = _connection.Dispatcher.QueryWriteAndWait(
49 | new Commands.QueueEnqueue(QueueName, key),
50 | static (c, s) => c.Execute(s));
51 |
52 | entry.SignalOneWaitNode();
53 | }
54 |
55 | void IDisposable.Dispose()
56 | {
57 | }
58 |
59 | void IFetchedJob.RemoveFromQueue()
60 | {
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/Hangfire.InMemory/InMemoryStorage.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.InMemory. Copyright © 2020 Hangfire OÜ.
2 | //
3 | // Hangfire is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU Lesser General Public License as
5 | // published by the Free Software Foundation, either version 3
6 | // of the License, or any later version.
7 | //
8 | // Hangfire is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU Lesser General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU Lesser General Public
14 | // License along with Hangfire. If not, see .
15 |
16 | using System;
17 | using System.Diagnostics.CodeAnalysis;
18 | #if !HANGFIRE_170
19 | using System.Collections.Generic;
20 | #endif
21 | using System.Globalization;
22 | using System.Threading;
23 | using Hangfire.Annotations;
24 | using Hangfire.InMemory.State;
25 | using Hangfire.Storage;
26 |
27 | namespace Hangfire.InMemory
28 | {
29 | ///
30 | /// A class that represents an in-memory job storage that stores all data
31 | /// related to background processing in a process' memory.
32 | ///
33 | [SuppressMessage("ReSharper", "RedundantNullnessAttributeWithNullableReferenceTypes", Justification = "Should be used for public classes")]
34 | public sealed class InMemoryStorage : JobStorage, IKeyProvider, IKeyProvider, IDisposable
35 | {
36 | private readonly Dispatcher? _guidDispatcher;
37 | private readonly Dispatcher? _longDispatcher;
38 |
39 | private PaddedInt64 _nextId;
40 |
41 | #if !HANGFIRE_170
42 | // These options don't relate to the defined storage comparison options
43 | private readonly Dictionary _features = new Dictionary(StringComparer.OrdinalIgnoreCase)
44 | {
45 | { "Storage.ExtendedApi", true },
46 | { "Job.Queue", true },
47 | { "Connection.GetUtcDateTime", true },
48 | { "Connection.BatchedGetFirstByLowestScoreFromSet", true },
49 | { "Connection.GetSetContains", true },
50 | { "Connection.GetSetCount.Limited", true },
51 | { "BatchedGetFirstByLowestScoreFromSet", true },
52 | { "Transaction.AcquireDistributedLock", true },
53 | { "Transaction.CreateJob", true },
54 | { "Transaction.SetJobParameter", true },
55 | { "TransactionalAcknowledge:InMemoryFetchedJob", true },
56 | { "Monitoring.DeletedStateGraphs", true },
57 | { "Monitoring.AwaitingJobs", true }
58 | };
59 | #endif
60 |
61 | ///
62 | /// Initializes a new instance of the class with default options.
63 | ///
64 | public InMemoryStorage()
65 | : this(new InMemoryStorageOptions())
66 | {
67 | }
68 |
69 | ///
70 | /// Initializes a new instance of the class with specified options.
71 | ///
72 | /// The options for the in-memory storage. Cannot be null.
73 | /// Thrown when the argument is null.
74 | public InMemoryStorage([NotNull] InMemoryStorageOptions options)
75 | {
76 | Options = options ?? throw new ArgumentNullException(nameof(options));
77 |
78 | switch (options.IdType)
79 | {
80 | case InMemoryStorageIdType.Guid:
81 | _guidDispatcher = new Dispatcher(
82 | "Hangfire:InMemoryDispatcher",
83 | MonotonicTime.GetCurrent,
84 | new MemoryState(Options.StringComparer, null))
85 | {
86 | CommandTimeout = Options.CommandTimeout
87 | };
88 | break;
89 | case InMemoryStorageIdType.Long:
90 | _longDispatcher = new Dispatcher(
91 | "Hangfire:InMemoryDispatcher",
92 | MonotonicTime.GetCurrent,
93 | new MemoryState(Options.StringComparer, null))
94 | {
95 | CommandTimeout = Options.CommandTimeout
96 | };
97 | break;
98 | default:
99 | throw new NotSupportedException(
100 | $"The given 'Options.IdType' value is not supported: {options.IdType:G}");
101 | }
102 | }
103 |
104 | ///
105 | public void Dispose()
106 | {
107 | _guidDispatcher?.Dispose();
108 | _longDispatcher?.Dispose();
109 | }
110 |
111 | ///
112 | /// Gets the options for the in-memory storage.
113 | ///
114 | public InMemoryStorageOptions Options { get; }
115 |
116 | ///
117 | /// Override of property. Always returns true for .
118 | ///
119 | public override bool LinearizableReads => true;
120 |
121 | #if !HANGFIRE_170
122 | ///
123 | public override bool HasFeature(string featureId)
124 | {
125 | if (featureId == null) throw new ArgumentNullException(nameof(featureId));
126 |
127 | return _features.TryGetValue(featureId, out var isSupported)
128 | ? isSupported
129 | : base.HasFeature(featureId);
130 | }
131 | #endif
132 |
133 | ///
134 | public override IMonitoringApi GetMonitoringApi()
135 | {
136 | if (_guidDispatcher != null)
137 | {
138 | return new InMemoryMonitoringApi(_guidDispatcher, this);
139 | }
140 |
141 | if (_longDispatcher != null)
142 | {
143 | return new InMemoryMonitoringApi(_longDispatcher, this);
144 | }
145 |
146 | throw new InvalidOperationException("Can not determine the dispatcher.");
147 | }
148 |
149 | ///
150 | public override IStorageConnection GetConnection()
151 | {
152 | if (_guidDispatcher != null)
153 | {
154 | return new InMemoryConnection(Options, _guidDispatcher, this);
155 | }
156 |
157 | if (_longDispatcher != null)
158 | {
159 | return new InMemoryConnection(Options, _longDispatcher, this);
160 | }
161 |
162 | throw new InvalidOperationException("Can not determine the dispatcher.");
163 | }
164 |
165 | ///
166 | public override string ToString()
167 | {
168 | return "In-Memory Storage";
169 | }
170 |
171 | Guid IKeyProvider.GetUniqueKey()
172 | {
173 | return Guid.NewGuid();
174 | }
175 |
176 | bool IKeyProvider.TryParse(string input, out Guid key)
177 | {
178 | return Guid.TryParse(input, out key);
179 | }
180 |
181 | string IKeyProvider.ToString(Guid key)
182 | {
183 | return key.ToString("D");
184 | }
185 |
186 | ulong IKeyProvider.GetUniqueKey()
187 | {
188 | return (ulong)Interlocked.Increment(ref _nextId.Value);
189 | }
190 |
191 | bool IKeyProvider.TryParse(string input, out ulong key)
192 | {
193 | return ulong.TryParse(input, NumberStyles.Integer, CultureInfo.InvariantCulture, out key);
194 | }
195 |
196 | string IKeyProvider.ToString(ulong key)
197 | {
198 | return key.ToString(CultureInfo.InvariantCulture);
199 | }
200 | }
201 | }
202 |
--------------------------------------------------------------------------------
/src/Hangfire.InMemory/InMemoryStorageIdType.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.InMemory. Copyright © 2024 Hangfire OÜ.
2 | //
3 | // Hangfire is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU Lesser General Public License as
5 | // published by the Free Software Foundation, either version 3
6 | // of the License, or any later version.
7 | //
8 | // Hangfire is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU Lesser General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU Lesser General Public
14 | // License along with Hangfire. If not, see .
15 |
16 | using System.Diagnostics.CodeAnalysis;
17 |
18 | namespace Hangfire.InMemory
19 | {
20 | ///
21 | /// Represents the type using for storing background job identifiers.
22 | ///
23 | [SuppressMessage("Naming", "CA1720:Identifier contains type name", Justification = "This is intentionally, by design.")]
24 | public enum InMemoryStorageIdType
25 | {
26 | ///
27 | /// Background job identifiers will be integer-based as in Hangfire.SqlServer storage.
28 | ///
29 | Long,
30 |
31 | ///
32 | /// Background job identifiers will be Guid-based like in Hangfire.Pro.Redis storage.
33 | ///
34 | Guid,
35 | }
36 | }
--------------------------------------------------------------------------------
/src/Hangfire.InMemory/InMemoryStorageOptions.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.InMemory. Copyright © 2020 Hangfire OÜ.
2 | //
3 | // Hangfire is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU Lesser General Public License as
5 | // published by the Free Software Foundation, either version 3
6 | // of the License, or any later version.
7 | //
8 | // Hangfire is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU Lesser General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU Lesser General Public
14 | // License along with Hangfire. If not, see .
15 |
16 | using System;
17 | using System.Threading;
18 |
19 | namespace Hangfire.InMemory
20 | {
21 | ///
22 | /// Provides configuration options for in-memory storage in Hangfire.
23 | ///
24 | public sealed class InMemoryStorageOptions
25 | {
26 | private int _maxStateHistoryLength = 10;
27 |
28 | ///
29 | /// Gets or sets the underlying key type for background jobs that can be useful
30 | /// to simulate different persistent storages.
31 | ///
32 | public InMemoryStorageIdType IdType { get; set; } = InMemoryStorageIdType.Long;
33 |
34 | ///
35 | /// Gets or sets the maximum expiration time for all the entries. When set, this
36 | /// value overrides any expiration time set in the other places of Hangfire. The
37 | /// main rationale for this is to control the amount of consumed RAM, since we are
38 | /// more limited in this case, especially when comparing to disk-based storages.
39 | ///
40 | public TimeSpan? MaxExpirationTime { get; set; } = TimeSpan.FromHours(3);
41 |
42 | ///
43 | /// Gets or sets the maximum length of state history for each background job. Older
44 | /// records are trimmed to avoid uncontrollable growth when some background job is
45 | /// constantly moved from one state to another without being completed.
46 | ///
47 | public int MaxStateHistoryLength
48 | {
49 | get => _maxStateHistoryLength;
50 | set
51 | {
52 | if (value <= 0) throw new ArgumentOutOfRangeException(nameof(value), "Value is out of range. Must be greater than zero.");
53 | _maxStateHistoryLength = value;
54 | }
55 | }
56 |
57 | ///
58 | /// Gets or sets comparison rules for keys and indexes inside the storage. You can use
59 | /// this option to match semantics of different storages, for example, use the
60 | /// value to match Redis' case-sensitive rules,
61 | /// or use the option to match SQL Server's
62 | /// default case-insensitive rules.
63 | ///
64 | public StringComparer StringComparer { get; set; } = StringComparer.Ordinal;
65 |
66 | ///
67 | /// Gets or sets the maximum time to wait for a command completion.
68 | ///
69 | public TimeSpan CommandTimeout { get; set; } = System.Diagnostics.Debugger.IsAttached ? Timeout.InfiniteTimeSpan : TimeSpan.FromSeconds(15);
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/Hangfire.InMemory/JobStorageMonitor.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.InMemory. Copyright © 2024 Hangfire OÜ.
2 | //
3 | // Hangfire is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU Lesser General Public License as
5 | // published by the Free Software Foundation, either version 3
6 | // of the License, or any later version.
7 | //
8 | // Hangfire is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU Lesser General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU Lesser General Public
14 | // License along with Hangfire. If not, see .
15 |
16 | using System;
17 | using System.Collections.Generic;
18 | using Hangfire.Storage;
19 | using Hangfire.Storage.Monitoring;
20 |
21 | namespace Hangfire.InMemory
22 | {
23 | internal abstract class JobStorageMonitor : IMonitoringApi
24 | {
25 | public abstract IList Queues();
26 | public abstract IList Servers();
27 | public abstract JobDetailsDto? JobDetails(string jobId);
28 | public abstract StatisticsDto GetStatistics();
29 | public abstract JobList EnqueuedJobs(string queue, int from, int perPage);
30 | public abstract JobList FetchedJobs(string queue, int from, int perPage);
31 | public abstract JobList ProcessingJobs(int from, int count);
32 | public abstract JobList ScheduledJobs(int from, int count);
33 | public abstract JobList SucceededJobs(int from, int count);
34 | public abstract JobList FailedJobs(int from, int count);
35 | public abstract JobList DeletedJobs(int from, int count);
36 | public abstract long ScheduledCount();
37 | public abstract long EnqueuedCount(string queue);
38 | public abstract long FetchedCount(string queue);
39 | public abstract long FailedCount();
40 | public abstract long ProcessingCount();
41 | public abstract long SucceededListCount();
42 | public abstract long DeletedListCount();
43 | public abstract IDictionary SucceededByDatesCount();
44 | public abstract IDictionary FailedByDatesCount();
45 | public abstract IDictionary HourlySucceededJobs();
46 | public abstract IDictionary HourlyFailedJobs();
47 | }
48 | }
--------------------------------------------------------------------------------
/src/Hangfire.InMemory/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | [assembly: AssemblyTitle("Hangfire.InMemory")]
6 | [assembly: AssemblyDescription("In-memory job storage for Hangfire with an efficient implementation.")]
7 | [assembly: Guid("0111B3E0-EB76-439B-969C-5C029ED74C51")]
8 | [assembly: InternalsVisibleTo("Hangfire.InMemory.Tests")]
9 |
10 | // Allow the generation of mocks for internal types
11 | [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
--------------------------------------------------------------------------------
/src/Hangfire.InMemory/PublicAPI.Shipped.txt:
--------------------------------------------------------------------------------
1 | #nullable enable
2 | Hangfire.GlobalConfigurationExtensions
3 | Hangfire.InMemory.InMemoryStorage
4 | Hangfire.InMemory.InMemoryStorage.Dispose() -> void
5 | Hangfire.InMemory.InMemoryStorage.InMemoryStorage() -> void
6 | Hangfire.InMemory.InMemoryStorage.InMemoryStorage(Hangfire.InMemory.InMemoryStorageOptions! options) -> void
7 | Hangfire.InMemory.InMemoryStorage.Options.get -> Hangfire.InMemory.InMemoryStorageOptions!
8 | Hangfire.InMemory.InMemoryStorageIdType
9 | Hangfire.InMemory.InMemoryStorageIdType.Guid = 1 -> Hangfire.InMemory.InMemoryStorageIdType
10 | Hangfire.InMemory.InMemoryStorageIdType.Long = 0 -> Hangfire.InMemory.InMemoryStorageIdType
11 | Hangfire.InMemory.InMemoryStorageOptions
12 | Hangfire.InMemory.InMemoryStorageOptions.CommandTimeout.get -> System.TimeSpan
13 | Hangfire.InMemory.InMemoryStorageOptions.CommandTimeout.set -> void
14 | Hangfire.InMemory.InMemoryStorageOptions.IdType.get -> Hangfire.InMemory.InMemoryStorageIdType
15 | Hangfire.InMemory.InMemoryStorageOptions.IdType.set -> void
16 | Hangfire.InMemory.InMemoryStorageOptions.InMemoryStorageOptions() -> void
17 | Hangfire.InMemory.InMemoryStorageOptions.MaxExpirationTime.get -> System.TimeSpan?
18 | Hangfire.InMemory.InMemoryStorageOptions.MaxExpirationTime.set -> void
19 | Hangfire.InMemory.InMemoryStorageOptions.MaxStateHistoryLength.get -> int
20 | Hangfire.InMemory.InMemoryStorageOptions.MaxStateHistoryLength.set -> void
21 | Hangfire.InMemory.InMemoryStorageOptions.StringComparer.get -> System.StringComparer!
22 | Hangfire.InMemory.InMemoryStorageOptions.StringComparer.set -> void
23 | override Hangfire.InMemory.InMemoryStorage.GetConnection() -> Hangfire.Storage.IStorageConnection!
24 | override Hangfire.InMemory.InMemoryStorage.GetMonitoringApi() -> Hangfire.Storage.IMonitoringApi!
25 | override Hangfire.InMemory.InMemoryStorage.HasFeature(string! featureId) -> bool
26 | override Hangfire.InMemory.InMemoryStorage.LinearizableReads.get -> bool
27 | override Hangfire.InMemory.InMemoryStorage.ToString() -> string!
28 | static Hangfire.GlobalConfigurationExtensions.UseInMemoryStorage(this Hangfire.IGlobalConfiguration! configuration) -> Hangfire.IGlobalConfiguration!
29 | static Hangfire.GlobalConfigurationExtensions.UseInMemoryStorage(this Hangfire.IGlobalConfiguration! configuration, Hangfire.InMemory.InMemoryStorageOptions! options) -> Hangfire.IGlobalConfiguration!
30 |
--------------------------------------------------------------------------------
/src/Hangfire.InMemory/PublicAPI.Unshipped.txt:
--------------------------------------------------------------------------------
1 | #nullable enable
2 |
--------------------------------------------------------------------------------
/src/Hangfire.InMemory/State/Dispatcher.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.InMemory. Copyright © 2020 Hangfire OÜ.
2 | //
3 | // Hangfire is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU Lesser General Public License as
5 | // published by the Free Software Foundation, either version 3
6 | // of the License, or any later version.
7 | //
8 | // Hangfire is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU Lesser General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU Lesser General Public
14 | // License along with Hangfire. If not, see .
15 |
16 | using System;
17 | using System.Collections.Concurrent;
18 | using System.Threading;
19 | using Hangfire.Logging;
20 |
21 | namespace Hangfire.InMemory.State
22 | {
23 | internal sealed class Dispatcher : DispatcherBase, IDisposable
24 | where TKey : IComparable
25 | {
26 | private const uint DefaultEvictionIntervalMs = 5000U;
27 |
28 | private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(0, 1);
29 |
30 | // ConcurrentBag for writes give much better throughput, but less stable, since some items are processed
31 | // with a heavy delay when new ones are constantly arriving.
32 | private readonly ConcurrentQueue> _queries = new ConcurrentQueue>();
33 | private readonly Thread _thread;
34 | private readonly ILog _logger = LogProvider.GetLogger(typeof(InMemoryStorage));
35 | private readonly CancellationTokenSource _cts = new CancellationTokenSource();
36 | private volatile bool _disposed;
37 |
38 | private PaddedInt64 _outstandingRequests;
39 |
40 | public Dispatcher(string threadName, Func timeResolver, MemoryState state) : base(timeResolver, state)
41 | {
42 | if (threadName == null) throw new ArgumentNullException(nameof(threadName));
43 |
44 | _thread = new Thread(DoWork)
45 | {
46 | IsBackground = true,
47 | Name = threadName
48 | };
49 | _thread.Start();
50 | }
51 |
52 | public TimeSpan CommandTimeout { get; init; } = Timeout.InfiniteTimeSpan;
53 |
54 | public void Dispose()
55 | {
56 | if (_disposed) return;
57 |
58 | _disposed = true;
59 | _cts.Cancel();
60 | _semaphore.Dispose();
61 | _thread.Join();
62 | _cts.Dispose();
63 | }
64 |
65 | public override T QueryWriteAndWait(TCommand query, Func, T> func)
66 | {
67 | if (_disposed) ThrowObjectDisposedException();
68 |
69 | using (var callback = new DispatcherCallback(query, func))
70 | {
71 | _queries.Enqueue(callback);
72 |
73 | if (Volatile.Read(ref _outstandingRequests.Value) == 0 &&
74 | Interlocked.Exchange(ref _outstandingRequests.Value, 1) == 0)
75 | {
76 | _semaphore.Release();
77 | }
78 |
79 | if (!callback.Wait(out var result, out var exception, CommandTimeout, _cts.Token))
80 | {
81 | throw new TimeoutException();
82 | }
83 |
84 | if (exception != null)
85 | {
86 | throw new InvalidOperationException("Dispatcher stopped due to an unhandled exception, storage state is corrupted.", exception);
87 | }
88 |
89 | return result!;
90 | }
91 | }
92 |
93 | public override T QueryReadAndWait(TCommand query, Func, T> func)
94 | {
95 | if (_disposed) ThrowObjectDisposedException();
96 |
97 | lock (_queries)
98 | {
99 | return func(query, State);
100 | }
101 | }
102 |
103 | private void DoWork()
104 | {
105 | try
106 | {
107 | var lastEviction = Environment.TickCount;
108 |
109 | while (!_disposed)
110 | {
111 | if (_semaphore.Wait(TimeSpan.FromMilliseconds(DefaultEvictionIntervalMs), _cts.Token))
112 | {
113 | Interlocked.Exchange(ref _outstandingRequests.Value, 0);
114 |
115 | while (_queries.TryDequeue(out var next))
116 | {
117 | lock (_queries)
118 | {
119 | next.Execute(State);
120 | }
121 |
122 | EvictExpiredEntriesIfNeeded(ref lastEviction);
123 | }
124 | }
125 |
126 | EvictExpiredEntriesIfNeeded(ref lastEviction);
127 | }
128 | }
129 | catch (OperationCanceledException ex) when (ex.CancellationToken == _cts.Token)
130 | {
131 | _logger.Debug("Query dispatcher has been gracefully stopped.");
132 | }
133 | catch (ObjectDisposedException ex) when (_disposed)
134 | {
135 | _logger.DebugException("Query dispatched stopped, because it was disposed.", ex);
136 | }
137 | catch (Exception ex) when (ExceptionHelper.IsCatchableExceptionType(ex))
138 | {
139 | _logger.FatalException("Query dispatcher stopped due to an exception, no queries will be processed. Please report this problem to Hangfire.InMemory developers.", ex);
140 | }
141 | }
142 |
143 | private void EvictExpiredEntriesIfNeeded(ref int lastEviction)
144 | {
145 | if (Environment.TickCount - lastEviction >= DefaultEvictionIntervalMs)
146 | {
147 | lock (_queries)
148 | {
149 | EvictExpiredEntries();
150 | }
151 |
152 | lastEviction = Environment.TickCount;
153 | }
154 | }
155 |
156 | private static void ThrowObjectDisposedException()
157 | {
158 | throw new ObjectDisposedException(typeof(Dispatcher).FullName);
159 | }
160 | }
161 | }
--------------------------------------------------------------------------------
/src/Hangfire.InMemory/State/DispatcherBase.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.InMemory. Copyright © 2020 Hangfire OÜ.
2 | //
3 | // Hangfire is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU Lesser General Public License as
5 | // published by the Free Software Foundation, either version 3
6 | // of the License, or any later version.
7 | //
8 | // Hangfire is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU Lesser General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU Lesser General Public
14 | // License along with Hangfire. If not, see .
15 |
16 | using System;
17 | using System.Collections.Generic;
18 | using System.Threading;
19 | using Hangfire.InMemory.Entities;
20 | using Hangfire.Storage;
21 |
22 | namespace Hangfire.InMemory.State
23 | {
24 | internal abstract class DispatcherBase
25 | where TKey : IComparable
26 | {
27 | private readonly Func _timeResolver;
28 | private readonly MemoryState _state;
29 |
30 | protected DispatcherBase(Func timeResolver, MemoryState state)
31 | {
32 | _timeResolver = timeResolver ?? throw new ArgumentNullException(nameof(timeResolver));
33 | _state = state ?? throw new ArgumentNullException(nameof(state));
34 | }
35 |
36 | protected MemoryState State => _state;
37 |
38 | public MonotonicTime GetMonotonicTime()
39 | {
40 | return _timeResolver();
41 | }
42 |
43 | public KeyValuePair>[] GetOrAddQueues(string[] queueNames)
44 | {
45 | var entries = new KeyValuePair>[queueNames.Length];
46 | var index = 0;
47 |
48 | foreach (var queueName in queueNames)
49 | {
50 | entries[index++] = new KeyValuePair>(
51 | queueName,
52 | _state.QueueGetOrAdd(queueName));
53 | }
54 |
55 | return entries;
56 | }
57 |
58 | public bool TryAcquireLockEntry(JobStorageConnection owner, string resource, TimeSpan timeout, out LockEntry? entry)
59 | {
60 | if (owner == null) throw new ArgumentNullException(nameof(owner));
61 | if (resource == null) throw new ArgumentNullException(nameof(resource));
62 |
63 | var spinWait = new SpinWait();
64 |
65 | while (true)
66 | {
67 | entry = _state.Locks.GetOrAdd(resource, static _ => new LockEntry());
68 | if (entry.TryAcquire(owner, timeout, out var retry, out var cleanUp))
69 | {
70 | return true;
71 | }
72 |
73 | if (cleanUp) CleanUpLockEntry(resource, entry);
74 | if (!retry) break;
75 |
76 | spinWait.SpinOnce();
77 | }
78 |
79 | entry = null;
80 | return false;
81 | }
82 |
83 | public void ReleaseLockEntry(JobStorageConnection owner, string resource, LockEntry entry)
84 | {
85 | if (owner == null) throw new ArgumentNullException(nameof(owner));
86 | if (resource == null) throw new ArgumentNullException(nameof(resource));
87 | if (entry == null) throw new ArgumentNullException(nameof(entry));
88 |
89 | entry.Release(owner, out var cleanUp);
90 |
91 | if (cleanUp) CleanUpLockEntry(resource, entry);
92 | }
93 |
94 | private void CleanUpLockEntry(string resource, LockEntry entry)
95 | {
96 | var hasRemoved = _state.Locks.TryRemove(resource, out var removed);
97 |
98 | // Workaround for issue https://github.com/dotnet/runtime/issues/107525, should be
99 | // removed after fix + some time.
100 | var spinWait = new SpinWait();
101 | while (!hasRemoved && _state.Locks.ContainsKey(resource))
102 | {
103 | hasRemoved = _state.Locks.TryRemove(resource, out removed);
104 | if (!hasRemoved) spinWait.SpinOnce();
105 | }
106 |
107 | try
108 | {
109 | if (!hasRemoved)
110 | {
111 | throw new InvalidOperationException("Wasn't able to remove a lock entry");
112 | }
113 |
114 | if (!ReferenceEquals(entry, removed))
115 | {
116 | throw new InvalidOperationException("Removed entry isn't the same as the requested one");
117 | }
118 | }
119 | finally
120 | {
121 | removed?.Dispose();
122 | }
123 | }
124 |
125 | public virtual T QueryWriteAndWait(TCommand query, Func, T> func)
126 | {
127 | return func(query, _state);
128 | }
129 |
130 | public virtual T QueryReadAndWait(TCommand query, Func, T> func)
131 | {
132 | return QueryWriteAndWait(query, func);
133 | }
134 |
135 | protected void EvictExpiredEntries()
136 | {
137 | _state.EvictExpiredEntries(GetMonotonicTime());
138 | }
139 | }
140 | }
--------------------------------------------------------------------------------
/src/Hangfire.InMemory/State/DispatcherCallback.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.InMemory. Copyright © 2020 Hangfire OÜ.
2 | //
3 | // Hangfire is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU Lesser General Public License as
5 | // published by the Free Software Foundation, either version 3
6 | // of the License, or any later version.
7 | //
8 | // Hangfire is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU Lesser General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU Lesser General Public
14 | // License along with Hangfire. If not, see .
15 |
16 | using System;
17 | using System.Threading;
18 |
19 | namespace Hangfire.InMemory.State
20 | {
21 | internal sealed class DispatcherCallback : IDispatcherCallback, IDisposable
22 | where TKey : IComparable
23 | {
24 | private readonly ManualResetEventSlim _ready = new ManualResetEventSlim(false);
25 | private readonly TCommand _command;
26 | private readonly Func, TResult> _func;
27 |
28 | private TResult? _result;
29 | private Exception? _exception;
30 |
31 | public DispatcherCallback(TCommand command, Func, TResult> func)
32 | {
33 | _command = command ?? throw new ArgumentNullException(nameof(command));
34 | _func = func ?? throw new ArgumentNullException(nameof(func));
35 | }
36 |
37 | public void Execute(MemoryState state)
38 | {
39 | try
40 | {
41 | var result = _func(_command, state);
42 |
43 | _result = result;
44 | _exception = null;
45 | TrySetReady();
46 | }
47 | catch (Exception ex) when (ExceptionHelper.IsCatchableExceptionType(ex))
48 | {
49 | _result = default;
50 | _exception = ex;
51 | TrySetReady();
52 |
53 | throw;
54 | }
55 | }
56 |
57 | public bool Wait(out TResult? result, out Exception? exception, TimeSpan timeout, CancellationToken token)
58 | {
59 | token.ThrowIfCancellationRequested();
60 |
61 | if (_ready.Wait(timeout, token))
62 | {
63 | result = _result;
64 | exception = _exception;
65 | return true;
66 | }
67 |
68 | result = default;
69 | exception = null;
70 | return false;
71 | }
72 |
73 | public void Dispose()
74 | {
75 | _ready.Dispose();
76 | }
77 |
78 | private void TrySetReady()
79 | {
80 | try
81 | {
82 | _ready.Set();
83 | }
84 | catch (ObjectDisposedException)
85 | {
86 | // Benign race condition, nothing to signal in this case.
87 | }
88 | }
89 | }
90 | }
--------------------------------------------------------------------------------
/src/Hangfire.InMemory/State/DispatcherExtensions.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.InMemory. Copyright © 2024 Hangfire OÜ.
2 | //
3 | // Hangfire is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU Lesser General Public License as
5 | // published by the Free Software Foundation, either version 3
6 | // of the License, or any later version.
7 | //
8 | // Hangfire is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU Lesser General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU Lesser General Public
14 | // License along with Hangfire. If not, see .
15 |
16 | using System;
17 |
18 | namespace Hangfire.InMemory.State
19 | {
20 | internal static class DispatcherExtensions
21 | {
22 | public static void QueryWriteAndWait(this DispatcherBase dispatcher, TCommand query)
23 | where TKey : IComparable
24 | where TCommand : ICommand
25 | {
26 | dispatcher.QueryWriteAndWait(query, static (q, s) =>
27 | {
28 | q.Execute(s);
29 | return true;
30 | });
31 | }
32 | }
33 | }
--------------------------------------------------------------------------------
/src/Hangfire.InMemory/State/ExtensionMethods.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.InMemory. Copyright © 2024 Hangfire OÜ.
2 | //
3 | // Hangfire is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU Lesser General Public License as
5 | // published by the Free Software Foundation, either version 3
6 | // of the License, or any later version.
7 | //
8 | // Hangfire is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU Lesser General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU Lesser General Public
14 | // License along with Hangfire. If not, see .
15 |
16 | using System;
17 | using System.ComponentModel;
18 | using System.Runtime.InteropServices;
19 | using Hangfire.Common;
20 | using Hangfire.Storage;
21 |
22 | namespace Hangfire.InMemory.State
23 | {
24 | [StructLayout(LayoutKind.Explicit, Size = 2 * CacheLineSize)]
25 | internal struct PaddedInt64
26 | {
27 | private const int CacheLineSize = 128;
28 |
29 | [FieldOffset(CacheLineSize)]
30 | internal long Value;
31 | }
32 |
33 | internal static class ExceptionHelper
34 | {
35 | #if !NETSTANDARD1_3
36 | private static readonly Type StackOverflowType = typeof(StackOverflowException);
37 | #endif
38 | private static readonly Type OutOfMemoryType = typeof(OutOfMemoryException);
39 |
40 | public static bool IsCatchableExceptionType(Exception ex)
41 | {
42 | var type = ex.GetType();
43 | return
44 | #if !NETSTANDARD1_3
45 | type != StackOverflowType &&
46 | #endif
47 | type != OutOfMemoryType;
48 | }
49 | }
50 |
51 | internal static class ExtensionMethods
52 | {
53 | public static Job? TryGetJob(this InvocationData? data, out JobLoadException? exception)
54 | {
55 | exception = null;
56 |
57 | try
58 | {
59 | if (data != null)
60 | {
61 | return data.DeserializeJob();
62 | }
63 | }
64 | catch (JobLoadException ex)
65 | {
66 | exception = ex;
67 | }
68 |
69 | return null;
70 | }
71 | }
72 | }
73 |
74 | namespace System.Runtime.CompilerServices
75 | {
76 | #if !NET5_0_OR_GREATER
77 |
78 | [EditorBrowsable(EditorBrowsableState.Never)]
79 | internal static class IsExternalInit {}
80 |
81 | #endif // !NET5_0_OR_GREATER
82 |
83 | #if !NET7_0_OR_GREATER
84 |
85 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
86 | internal sealed class RequiredMemberAttribute : Attribute {}
87 |
88 | [AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)]
89 | internal sealed class CompilerFeatureRequiredAttribute : Attribute
90 | {
91 | public CompilerFeatureRequiredAttribute(string featureName)
92 | {
93 | FeatureName = featureName;
94 | }
95 |
96 | public string FeatureName { get; }
97 | public bool IsOptional { get; init; }
98 |
99 | public const string RefStructs = nameof(RefStructs);
100 | public const string RequiredMembers = nameof(RequiredMembers);
101 | }
102 |
103 | #endif // !NET7_0_OR_GREATER
104 | }
105 |
106 | namespace System.Diagnostics.CodeAnalysis
107 | {
108 | #if !NET7_0_OR_GREATER
109 | [AttributeUsage(AttributeTargets.Constructor, AllowMultiple = false, Inherited = false)]
110 | internal sealed class SetsRequiredMembersAttribute : Attribute {}
111 | #endif
112 | }
--------------------------------------------------------------------------------
/src/Hangfire.InMemory/State/ICommand.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.InMemory. Copyright © 2024 Hangfire OÜ.
2 | //
3 | // Hangfire is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU Lesser General Public License as
5 | // published by the Free Software Foundation, either version 3
6 | // of the License, or any later version.
7 | //
8 | // Hangfire is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU Lesser General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU Lesser General Public
14 | // License along with Hangfire. If not, see .
15 |
16 | using System;
17 |
18 | namespace Hangfire.InMemory.State
19 | {
20 | internal interface ICommand where TKey : IComparable
21 | {
22 | void Execute(MemoryState state);
23 | }
24 | }
--------------------------------------------------------------------------------
/src/Hangfire.InMemory/State/IDispatcherCallback.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.InMemory. Copyright © 2024 Hangfire OÜ.
2 | //
3 | // Hangfire is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU Lesser General Public License as
5 | // published by the Free Software Foundation, either version 3
6 | // of the License, or any later version.
7 | //
8 | // Hangfire is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU Lesser General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU Lesser General Public
14 | // License along with Hangfire. If not, see .
15 |
16 | using System;
17 |
18 | namespace Hangfire.InMemory.State
19 | {
20 | internal interface IDispatcherCallback
21 | where TKey : IComparable
22 | {
23 | void Execute(MemoryState state);
24 | }
25 | }
--------------------------------------------------------------------------------
/src/Hangfire.InMemory/State/MonotonicTime.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.InMemory. Copyright © 2020 Hangfire OÜ.
2 | //
3 | // Hangfire is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU Lesser General Public License as
5 | // published by the Free Software Foundation, either version 3
6 | // of the License, or any later version.
7 | //
8 | // Hangfire is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU Lesser General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU Lesser General Public
14 | // License along with Hangfire. If not, see .
15 |
16 | using System;
17 | using System.Diagnostics;
18 | using System.Globalization;
19 |
20 | namespace Hangfire.InMemory.State
21 | {
22 | [DebuggerDisplay("{DebuggerToString()}")]
23 | internal readonly struct MonotonicTime : IEquatable, IComparable, IComparable
24 | {
25 | private const long TicksPerMillisecond = 10000;
26 | private const long TicksPerSecond = TicksPerMillisecond * 1000;
27 |
28 | private static readonly double TickFrequency = (double)TicksPerSecond / Stopwatch.Frequency;
29 |
30 | private readonly long _timestamp;
31 |
32 | private MonotonicTime(long timestamp)
33 | {
34 | _timestamp = timestamp;
35 | }
36 |
37 | public static MonotonicTime GetCurrent()
38 | {
39 | return new MonotonicTime(Stopwatch.GetTimestamp());
40 | }
41 |
42 | public MonotonicTime Add(TimeSpan value)
43 | {
44 | return new MonotonicTime(_timestamp + unchecked((long)(value.Ticks / TickFrequency)));
45 | }
46 |
47 | public DateTime ToUtcDateTime()
48 | {
49 | return DateTime.UtcNow.Add(this - GetCurrent());
50 | }
51 |
52 | public override bool Equals(object? obj)
53 | {
54 | return obj is MonotonicTime other && Equals(other);
55 | }
56 |
57 | public bool Equals(MonotonicTime other)
58 | {
59 | return _timestamp == other._timestamp;
60 | }
61 |
62 | public override int GetHashCode()
63 | {
64 | return _timestamp.GetHashCode();
65 | }
66 |
67 | public int CompareTo(object? obj)
68 | {
69 | if (obj == null) return 1;
70 | if (obj is not MonotonicTime time)
71 | {
72 | throw new ArgumentException("Value must be of type " + nameof(MonotonicTime), nameof(obj));
73 | }
74 |
75 | return CompareTo(time);
76 | }
77 |
78 | public int CompareTo(MonotonicTime other)
79 | {
80 | return _timestamp.CompareTo(other._timestamp);
81 | }
82 |
83 | public override string ToString()
84 | {
85 | return _timestamp.ToString(CultureInfo.InvariantCulture);
86 | }
87 |
88 | public static TimeSpan operator -(MonotonicTime left, MonotonicTime right)
89 | {
90 | var elapsed = unchecked((long)((left._timestamp - right._timestamp) * TickFrequency));
91 | return new TimeSpan(elapsed);
92 | }
93 |
94 | public static bool operator ==(MonotonicTime left, MonotonicTime right) => left.Equals(right);
95 | public static bool operator !=(MonotonicTime left, MonotonicTime right) => !(left == right);
96 | public static bool operator <(MonotonicTime left, MonotonicTime right) => left.CompareTo(right) < 0;
97 | public static bool operator <=(MonotonicTime left, MonotonicTime right) => left.CompareTo(right) <= 0;
98 | public static bool operator >(MonotonicTime left, MonotonicTime right) => left.CompareTo(right) > 0;
99 | public static bool operator >=(MonotonicTime left, MonotonicTime right) => left.CompareTo(right) >= 0;
100 |
101 | private string DebuggerToString()
102 | {
103 | return $"DateTime: {ToUtcDateTime()}, Raw: {ToString()}";
104 | }
105 | }
106 | }
--------------------------------------------------------------------------------
/src/Hangfire.InMemory/State/Queries.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.InMemory. Copyright © 2024 Hangfire OÜ.
2 | //
3 | // Hangfire is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU Lesser General Public License as
5 | // published by the Free Software Foundation, either version 3
6 | // of the License, or any later version.
7 | //
8 | // Hangfire is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU Lesser General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU Lesser General Public
14 | // License along with Hangfire. If not, see .
15 |
16 | using System;
17 | using System.Collections.Generic;
18 | using System.Linq;
19 | using Hangfire.Storage;
20 |
21 | namespace Hangfire.InMemory.State
22 | {
23 | internal static class Queries where TKey : IComparable
24 | {
25 | public readonly struct JobGetData(TKey key)
26 | {
27 | public Data? Execute(MemoryState state)
28 | {
29 | if (!state.Jobs.TryGetValue(key, out var entry))
30 | {
31 | return null;
32 | }
33 |
34 | return new Data
35 | {
36 | InvocationData = entry.InvocationData,
37 | State = entry.State?.Name,
38 | CreatedAt = entry.CreatedAt,
39 | Parameters = entry.GetParameters(),
40 | StringComparer = state.StringComparer
41 | };
42 | }
43 |
44 | public readonly struct Data
45 | {
46 | public required InvocationData InvocationData { get; init; }
47 | public required string? State { get; init; }
48 | public required MonotonicTime CreatedAt { get; init; }
49 | public required KeyValuePair[] Parameters { get; init; }
50 | public required StringComparer StringComparer { get; init; }
51 | }
52 | }
53 |
54 | public readonly struct JobGetState(TKey key)
55 | {
56 | public Data? Execute(MemoryState state)
57 | {
58 | if (!state.Jobs.TryGetValue(key, out var entry) || entry.State == null)
59 | {
60 | return null;
61 | }
62 |
63 | return new Data
64 | {
65 | Name = entry.State.Name,
66 | Reason = entry.State.Reason,
67 | StateData = entry.State.Data,
68 | StringComparer = state.StringComparer
69 | };
70 | }
71 |
72 | public readonly struct Data
73 | {
74 | public required string Name { get; init; }
75 | public required string? Reason { get; init; }
76 | public required KeyValuePair[] StateData { get; init; }
77 | public required StringComparer StringComparer { get; init; }
78 | }
79 | }
80 |
81 | public readonly struct JobGetParameter(TKey key, string name)
82 | {
83 | public string? Execute(MemoryState state)
84 | {
85 | return state.Jobs.TryGetValue(key, out var entry)
86 | ? entry.GetParameter(name, state.StringComparer)
87 | : null;
88 | }
89 | }
90 |
91 | public readonly struct SortedSetGetAll(string key)
92 | {
93 | public HashSet Execute(MemoryState state)
94 | {
95 | var result = new HashSet(state.StringComparer);
96 |
97 | if (state.Sets.TryGetValue(key, out var entry))
98 | {
99 | foreach (var item in entry)
100 | {
101 | result.Add(item.Value);
102 | }
103 | }
104 |
105 | return result;
106 | }
107 | }
108 |
109 | public readonly struct SortedSetFirstByLowestScore(string key, double fromScore, double toScore)
110 | {
111 | public string? Execute(MemoryState state)
112 | {
113 | if (state.Sets.TryGetValue(key, out var entry))
114 | {
115 | return entry.GetFirstBetween(fromScore, toScore);
116 | }
117 |
118 | return null;
119 | }
120 | }
121 |
122 | public readonly struct SortedSetFirstByLowestScoreMultiple(string key, double fromScore, double toScore, int count)
123 | {
124 | public List Execute(MemoryState state)
125 | {
126 | if (state.Sets.TryGetValue(key, out var entry))
127 | {
128 | return entry.GetViewBetween(fromScore, toScore, count);
129 | }
130 |
131 | return new List();
132 | }
133 | }
134 |
135 | public readonly struct SortedSetRange(string key, int startingFrom, int endingAt)
136 | {
137 | public List Execute(MemoryState state)
138 | {
139 | var result = new List();
140 |
141 | if (state.Sets.TryGetValue(key, out var entry))
142 | {
143 | var counter = 0;
144 |
145 | foreach (var item in entry)
146 | {
147 | if (counter < startingFrom) { counter++; continue; }
148 | if (counter > endingAt) break;
149 |
150 | result.Add(item.Value);
151 |
152 | counter++;
153 | }
154 | }
155 |
156 | return result;
157 | }
158 | }
159 |
160 | public readonly struct SortedSetContains(string key, string value)
161 | {
162 | public bool Execute(MemoryState state)
163 | {
164 | return state.Sets.TryGetValue(key, out var entry) && entry.Contains(value);
165 | }
166 | }
167 |
168 | public readonly struct SortedSetCount(string key)
169 | {
170 | public int Execute(MemoryState state)
171 | {
172 | return state.Sets.TryGetValue(key, out var entry) ? entry.Count : 0;
173 | }
174 | }
175 |
176 | public readonly struct SortedSetCountMultiple(IEnumerable keys, int limit)
177 | {
178 | public int Execute(MemoryState state)
179 | {
180 | var count = 0;
181 |
182 | foreach (var key in keys)
183 | {
184 | if (count >= limit) break;
185 | count += state.Sets.TryGetValue(key, out var entry) ? entry.Count : 0;
186 | }
187 |
188 | return Math.Min(count, limit);
189 | }
190 | }
191 |
192 | public readonly struct SortedSetTimeToLive(string key)
193 | {
194 | public MonotonicTime? Execute(MemoryState state)
195 | {
196 | if (state.Sets.TryGetValue(key, out var entry) && entry.ExpireAt.HasValue)
197 | {
198 | return entry.ExpireAt;
199 | }
200 |
201 | return null;
202 | }
203 | }
204 |
205 | public readonly struct HashGetAll(string key)
206 | {
207 | public Dictionary? Execute(MemoryState state)
208 | {
209 | if (state.Hashes.TryGetValue(key, out var entry))
210 | {
211 | return entry.Value.ToDictionary(static x => x.Key, static x => x.Value, state.StringComparer);
212 | }
213 |
214 | return null;
215 | }
216 | }
217 |
218 | public readonly struct HashGet(string key, string name)
219 | {
220 | public string? Execute(MemoryState state)
221 | {
222 | if (state.Hashes.TryGetValue(key, out var entry) && entry.Value.TryGetValue(name, out var result))
223 | {
224 | return result;
225 | }
226 |
227 | return null;
228 | }
229 | }
230 |
231 | public readonly struct HashFieldCount(string key)
232 | {
233 | public int Execute(MemoryState state)
234 | {
235 | return state.Hashes.TryGetValue(key, out var entry) ? entry.Value.Count : 0;
236 | }
237 | }
238 |
239 | public readonly struct HashTimeToLive(string key)
240 | {
241 | public MonotonicTime? Execute(MemoryState state)
242 | {
243 | if (state.Hashes.TryGetValue(key, out var entry) && entry.ExpireAt.HasValue)
244 | {
245 | return entry.ExpireAt;
246 | }
247 |
248 | return null;
249 | }
250 | }
251 |
252 | public readonly struct ListGetAll(string key)
253 | {
254 | public List Execute(MemoryState state)
255 | {
256 | if (state.Lists.TryGetValue(key, out var entry))
257 | {
258 | return new List(entry);
259 | }
260 |
261 | return new List();
262 | }
263 | }
264 |
265 | public readonly struct ListRange(string key, int startingFrom, int endingAt)
266 | {
267 | public List Execute(MemoryState state)
268 | {
269 | var result = new List();
270 |
271 | if (state.Lists.TryGetValue(key, out var entry))
272 | {
273 | var count = endingAt - startingFrom + 1;
274 | var skip = startingFrom;
275 | foreach (var item in entry)
276 | {
277 | if (skip-- > 0) continue;
278 | if (count-- == 0) break;
279 |
280 | result.Add(item);
281 | }
282 | }
283 |
284 | return result;
285 | }
286 | }
287 |
288 | public readonly struct ListCount(string key)
289 | {
290 | public int Execute(MemoryState state)
291 | {
292 | return state.Lists.TryGetValue(key, out var entry) ? entry.Count : 0;
293 | }
294 | }
295 |
296 | public readonly struct ListTimeToLive(string key)
297 | {
298 | public MonotonicTime? Execute(MemoryState state)
299 | {
300 | if (state.Lists.TryGetValue(key, out var entry) && entry.ExpireAt.HasValue)
301 | {
302 | return entry.ExpireAt;
303 | }
304 |
305 | return null;
306 | }
307 | }
308 |
309 | public readonly struct CounterGet(string key)
310 | {
311 | public long Execute(MemoryState state)
312 | {
313 | return state.Counters.TryGetValue(key, out var entry) ? entry.Value : 0;
314 | }
315 | }
316 | }
317 | }
--------------------------------------------------------------------------------
/src/Hangfire.InMemory/packages.lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 1,
3 | "dependencies": {
4 | ".NETFramework,Version=v4.5.1": {
5 | "Hangfire.Core": {
6 | "type": "Direct",
7 | "requested": "[1.8.0, )",
8 | "resolved": "1.8.0",
9 | "contentHash": "YyQwi1iKCS4HsKnwUhY5dcyxOeJ0MqA/0gjeTJdMsCXufKl73I+y8mS5MbvQBIKMGcjv0FYzjLA+v31P6G+CRw==",
10 | "dependencies": {
11 | "Newtonsoft.Json": "5.0.1",
12 | "Owin": "1.0.0"
13 | }
14 | },
15 | "Microsoft.CodeAnalysis.NetAnalyzers": {
16 | "type": "Direct",
17 | "requested": "[9.0.0, )",
18 | "resolved": "9.0.0",
19 | "contentHash": "JajbvkrBgtdRghavIjcJuNHMOja4lqBmEezbhZyqWPYh2cpLhT5mPpfC7NQVDO4IehWQum9t/nwF4v+qQGtYWg=="
20 | },
21 | "Microsoft.CodeAnalysis.PublicApiAnalyzers": {
22 | "type": "Direct",
23 | "requested": "[3.3.4, )",
24 | "resolved": "3.3.4",
25 | "contentHash": "kNLTfXtXUWDHVt5iaPkkiPuyHYlMgLI6SOFT4w88bfeI2vqSeGgHunFkdvlaCM8RDfcY0t2+jnesQtidRJJ/DA=="
26 | },
27 | "Microsoft.NETFramework.ReferenceAssemblies": {
28 | "type": "Direct",
29 | "requested": "[1.0.3, )",
30 | "resolved": "1.0.3",
31 | "contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==",
32 | "dependencies": {
33 | "Microsoft.NETFramework.ReferenceAssemblies.net451": "1.0.3"
34 | }
35 | },
36 | "Microsoft.SourceLink.GitHub": {
37 | "type": "Direct",
38 | "requested": "[8.0.0, )",
39 | "resolved": "8.0.0",
40 | "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==",
41 | "dependencies": {
42 | "Microsoft.Build.Tasks.Git": "8.0.0",
43 | "Microsoft.SourceLink.Common": "8.0.0"
44 | }
45 | },
46 | "Microsoft.Build.Tasks.Git": {
47 | "type": "Transitive",
48 | "resolved": "8.0.0",
49 | "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
50 | },
51 | "Microsoft.NETFramework.ReferenceAssemblies.net451": {
52 | "type": "Transitive",
53 | "resolved": "1.0.3",
54 | "contentHash": "vVPinxdLrwoX81ApbNIHDBI6qymQEy8eSOxDNBgKJtc2+cifnF0oT1U2d3EFx+V5O68yaqna2myZJNsgKCpVkA=="
55 | },
56 | "Microsoft.SourceLink.Common": {
57 | "type": "Transitive",
58 | "resolved": "8.0.0",
59 | "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
60 | },
61 | "Newtonsoft.Json": {
62 | "type": "Transitive",
63 | "resolved": "5.0.1",
64 | "contentHash": "AuSDf0kpGGLSvFmj1Zia8BxTeUCdQ6lB8lWUZRYVXRnAQLmiEGmoP0M+9KHwJNqBW2FiFwSG8Jkz3G7tS6k7MQ=="
65 | },
66 | "Owin": {
67 | "type": "Transitive",
68 | "resolved": "1.0.0",
69 | "contentHash": "OseTFniKmyp76mEzOBwIKGBRS5eMoYNkMKaMXOpxx9jv88+b6mh1rSaw43vjBOItNhaLFG3d0a20PfHyibH5sw=="
70 | }
71 | },
72 | ".NETStandard,Version=v2.0": {
73 | "Hangfire.Core": {
74 | "type": "Direct",
75 | "requested": "[1.8.0, )",
76 | "resolved": "1.8.0",
77 | "contentHash": "YyQwi1iKCS4HsKnwUhY5dcyxOeJ0MqA/0gjeTJdMsCXufKl73I+y8mS5MbvQBIKMGcjv0FYzjLA+v31P6G+CRw==",
78 | "dependencies": {
79 | "Newtonsoft.Json": "11.0.1"
80 | }
81 | },
82 | "Microsoft.CodeAnalysis.NetAnalyzers": {
83 | "type": "Direct",
84 | "requested": "[9.0.0, )",
85 | "resolved": "9.0.0",
86 | "contentHash": "JajbvkrBgtdRghavIjcJuNHMOja4lqBmEezbhZyqWPYh2cpLhT5mPpfC7NQVDO4IehWQum9t/nwF4v+qQGtYWg=="
87 | },
88 | "Microsoft.CodeAnalysis.PublicApiAnalyzers": {
89 | "type": "Direct",
90 | "requested": "[3.3.4, )",
91 | "resolved": "3.3.4",
92 | "contentHash": "kNLTfXtXUWDHVt5iaPkkiPuyHYlMgLI6SOFT4w88bfeI2vqSeGgHunFkdvlaCM8RDfcY0t2+jnesQtidRJJ/DA=="
93 | },
94 | "Microsoft.SourceLink.GitHub": {
95 | "type": "Direct",
96 | "requested": "[8.0.0, )",
97 | "resolved": "8.0.0",
98 | "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==",
99 | "dependencies": {
100 | "Microsoft.Build.Tasks.Git": "8.0.0",
101 | "Microsoft.SourceLink.Common": "8.0.0"
102 | }
103 | },
104 | "NETStandard.Library": {
105 | "type": "Direct",
106 | "requested": "[2.0.3, )",
107 | "resolved": "2.0.3",
108 | "contentHash": "st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==",
109 | "dependencies": {
110 | "Microsoft.NETCore.Platforms": "1.1.0"
111 | }
112 | },
113 | "Microsoft.Build.Tasks.Git": {
114 | "type": "Transitive",
115 | "resolved": "8.0.0",
116 | "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
117 | },
118 | "Microsoft.NETCore.Platforms": {
119 | "type": "Transitive",
120 | "resolved": "1.1.0",
121 | "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A=="
122 | },
123 | "Microsoft.SourceLink.Common": {
124 | "type": "Transitive",
125 | "resolved": "8.0.0",
126 | "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
127 | },
128 | "Newtonsoft.Json": {
129 | "type": "Transitive",
130 | "resolved": "11.0.1",
131 | "contentHash": "pNN4l+J6LlpIvHOeNdXlwxv39NPJ2B5klz+Rd2UQZIx30Squ5oND1Yy3wEAUoKn0GPUj6Yxt9lxlYWQqfZcvKg=="
132 | }
133 | }
134 | }
135 | }
--------------------------------------------------------------------------------
/src/SharedAssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Reflection;
3 | using System.Runtime.InteropServices;
4 |
5 | [assembly: AssemblyProduct("Hangfire")]
6 | [assembly: AssemblyCompany("Hangfire OÜ")]
7 | [assembly: AssemblyCopyright("Copyright © 2020-2024 Hangfire OÜ")]
8 | [assembly: AssemblyCulture("")]
9 |
10 | [assembly: ComVisible(false)]
11 | [assembly: CLSCompliant(true)]
12 |
13 | // Please don't edit it manually, use the `build.bat version` command instead.
14 | [assembly: AssemblyVersion("1.0.0")]
15 |
--------------------------------------------------------------------------------
/tests/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | false
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/tests/Hangfire.InMemory.Tests/Entities/ExpirableEntryComparerFacts.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.InMemory. Copyright © 2023 Hangfire OÜ.
2 | //
3 | // Hangfire is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU Lesser General Public License as
5 | // published by the Free Software Foundation, either version 3
6 | // of the License, or any later version.
7 | //
8 | // Hangfire is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU Lesser General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU Lesser General Public
14 | // License along with Hangfire. If not, see .
15 |
16 | using System;
17 | using System.Linq;
18 | using Hangfire.InMemory.Entities;
19 | using Hangfire.InMemory.State;
20 | using Xunit;
21 |
22 | namespace Hangfire.InMemory.Tests.Entities
23 | {
24 | public class ExpirableEntryComparerFacts
25 | {
26 | private readonly StringComparer _stringComparer = StringComparer.Ordinal;
27 |
28 | [Fact]
29 | public void Ctor_DoesNotThrowAnException_WhenComparerIsNull()
30 | {
31 | var comparer = new ExpirableEntryComparer(null);
32 | Assert.NotNull(comparer);
33 | }
34 |
35 | [Fact]
36 | public void Compare_ReturnsZero_WhenBothEntries_AreNull()
37 | {
38 | var comparer = CreateComparer();
39 |
40 | var result = comparer.Compare(null, null);
41 |
42 | Assert.Equal(0, result);
43 | }
44 |
45 | [Fact]
46 | public void Compare_ReturnsPlusOne_WhenXIsNull_AndYIsNotNull()
47 | {
48 | var comparer = CreateComparer();
49 | var entry = new ExpirableEntryStub(null, null);
50 |
51 | var result = comparer.Compare(null, entry);
52 |
53 | Assert.Equal(+1, result);
54 | }
55 |
56 | [Fact]
57 | public void Compare_ReturnsMinusOne_WhenXIsNotNull_AndYIsNull()
58 | {
59 | var comparer = CreateComparer();
60 | var entry = new ExpirableEntryStub(null, null);
61 |
62 | var result = comparer.Compare(entry, null);
63 |
64 | Assert.Equal(-1, result);
65 | }
66 |
67 | [Fact]
68 | public void Compare_ReturnsZero_ForSameEntries_WithNullExpireAt()
69 | {
70 | var comparer = CreateComparer();
71 | var entry = new ExpirableEntryStub(null, null);
72 |
73 | var result = comparer.Compare(entry, entry);
74 |
75 | Assert.Equal(0, result);
76 | }
77 |
78 | [Fact]
79 | public void Compare_ReturnsZero_ForSameEntries_WithNonNullExpireAt()
80 | {
81 | var comparer = CreateComparer();
82 | var entry = new ExpirableEntryStub("key", MonotonicTime.GetCurrent());
83 |
84 | var result = comparer.Compare(entry, entry);
85 |
86 | Assert.Equal(0, result);
87 | }
88 |
89 | [Fact]
90 | public void Compare_ReturnsZero_ForEntries_WithTheSameKey_AndSameNonNullExpireAt()
91 | {
92 | var comparer = CreateComparer();
93 | var now = MonotonicTime.GetCurrent();
94 |
95 | var result = comparer.Compare(
96 | new ExpirableEntryStub("key", now),
97 | new ExpirableEntryStub("key", now));
98 |
99 | Assert.Equal(0, result);
100 | }
101 |
102 | [Fact]
103 | public void Compare_ReturnsZero_ForEntries_WithNullKey_AndSameNonNullExpireAt()
104 | {
105 | var comparer = CreateComparer();
106 | var now = MonotonicTime.GetCurrent();
107 |
108 | var result = comparer.Compare(
109 | new ExpirableEntryStub(null, now),
110 | new ExpirableEntryStub(null, now));
111 |
112 | Assert.Equal(0, result);
113 | }
114 |
115 | [Fact]
116 | public void Compare_ReturnsZero_ForEntries_WithTheSameNonNullKey_AndNullExpireAt()
117 | {
118 | var comparer = CreateComparer();
119 |
120 | var result = comparer.Compare(
121 | new ExpirableEntryStub("key", null),
122 | new ExpirableEntryStub("key", null));
123 |
124 | Assert.Equal(0, result);
125 | }
126 |
127 | [Fact]
128 | public void Compare_ReturnsZero_ForEntries_WithNullKey_AndNullExpireAt()
129 | {
130 | var comparer = CreateComparer();
131 |
132 | var result = comparer.Compare(
133 | new ExpirableEntryStub(null, null),
134 | new ExpirableEntryStub(null, null));
135 |
136 | Assert.Equal(0, result);
137 | }
138 |
139 | [Fact]
140 | public void Compare_ReturnsPlusOne_WhenXIsNull_AndYIsNot()
141 | {
142 | var comparer = CreateComparer();
143 | var now = MonotonicTime.GetCurrent();
144 | var x = new ExpirableEntryStub("key-1", null);
145 |
146 | Assert.Equal(-1, _stringComparer.Compare("key-1", "key-2")); // Just to check
147 | Assert.Equal(+1, comparer.Compare(x, new ExpirableEntryStub("key-2", now)));
148 | Assert.Equal(+1, comparer.Compare(x, new ExpirableEntryStub("key-1", now)));
149 | Assert.Equal(+1, comparer.Compare(x, new ExpirableEntryStub(null, now)));
150 | }
151 |
152 | [Fact]
153 | public void Compare_ReturnsPlusOne_WhenXExpireAt_IsGreaterThan_YExpireAt()
154 | {
155 | var comparer = CreateComparer();
156 | var now = MonotonicTime.GetCurrent();
157 | var x = new ExpirableEntryStub("key", now);
158 |
159 | Assert.Equal(+1, comparer.Compare(x, new ExpirableEntryStub("key", now.Add(TimeSpan.FromSeconds(-1)))));
160 | }
161 |
162 | [Fact]
163 | public void Compare_ReturnsMinusOne_WhenYIsNull_AndXIsNot()
164 | {
165 | var comparer = CreateComparer();
166 | var now = MonotonicTime.GetCurrent();
167 | var y = new ExpirableEntryStub("key-1", null);
168 |
169 | Assert.Equal(+1, _stringComparer.Compare("key-2", "key-1")); // Just to check
170 | Assert.Equal(-1, comparer.Compare(new ExpirableEntryStub("key-2", now), y));
171 | Assert.Equal(-1, comparer.Compare(new ExpirableEntryStub("key-1", now), y));
172 | Assert.Equal(-1, comparer.Compare(new ExpirableEntryStub(null, now), y));
173 | }
174 |
175 | [Fact]
176 | public void Compare_ReturnsMinusOne_WhenXExpireAt_IsLessThan_YExpireAt()
177 | {
178 | var comparer = CreateComparer();
179 | var now = MonotonicTime.GetCurrent();
180 | var x = new ExpirableEntryStub("key", now);
181 |
182 | Assert.Equal(-1, comparer.Compare(x, new ExpirableEntryStub("key", now.Add(TimeSpan.FromSeconds(1)))));
183 | }
184 |
185 | [Fact]
186 | public void Compare_FallsBackToComparer_WhenItPassed_WhenExpireAtAreEqual_OrNull()
187 | {
188 | var comparer = CreateComparer();
189 | var now = MonotonicTime.GetCurrent();
190 |
191 | Assert.Equal(-1, comparer.Compare(
192 | new ExpirableEntryStub("key-1", now),
193 | new ExpirableEntryStub("key-2", now)));
194 |
195 | Assert.Equal(+1, comparer.Compare(
196 | new ExpirableEntryStub("key-2", now),
197 | new ExpirableEntryStub("key-1", now)));
198 |
199 | Assert.Equal(-1, comparer.Compare(
200 | new ExpirableEntryStub("key-1", null),
201 | new ExpirableEntryStub("key-2", null)));
202 |
203 | Assert.Equal(+1, comparer.Compare(
204 | new ExpirableEntryStub("key-2", null),
205 | new ExpirableEntryStub("key-1", null)));
206 | }
207 |
208 | [Fact]
209 | public void Compare_FallsBackToEntities_WhenItPassed_WhenExpireAtAreEqual_OrNull()
210 | {
211 | var comparer = new ExpirableEntryComparer(null);
212 | var now = MonotonicTime.GetCurrent();
213 |
214 | Assert.Equal(-1, comparer.Compare(
215 | new ExpirableEntryStub(1, now),
216 | new ExpirableEntryStub(2, now)));
217 |
218 | Assert.Equal(+1, comparer.Compare(
219 | new ExpirableEntryStub(2, now),
220 | new ExpirableEntryStub(1, now)));
221 |
222 | Assert.Equal(-1, comparer.Compare(
223 | new ExpirableEntryStub(1, null),
224 | new ExpirableEntryStub(2, null)));
225 |
226 | Assert.Equal(+1, comparer.Compare(
227 | new ExpirableEntryStub(2, null),
228 | new ExpirableEntryStub(1, null)));
229 | }
230 |
231 | [Fact]
232 | public void Compare_LeadsToSorting_InTheAscendingOrder_OfExpireAtValues()
233 | {
234 | var now = MonotonicTime.GetCurrent();
235 | var array = new []
236 | {
237 | new ExpirableEntryStub("key", now),
238 | new ExpirableEntryStub("key", now.Add(TimeSpan.FromHours(-1))),
239 | new ExpirableEntryStub("key", now.Add(TimeSpan.FromDays(1)))
240 | };
241 |
242 | var comparer = CreateComparer();
243 | var result = array.OrderBy(x => x, comparer).ToArray();
244 |
245 | Assert.Equal(new [] { array[1], array[0], array[2] }, result);
246 | }
247 |
248 | [Fact]
249 | public void Compare_PlacesNullsLast_WhenSortingByExpireAtValues()
250 | {
251 | var now = MonotonicTime.GetCurrent();
252 | var array = new[]
253 | {
254 | new ExpirableEntryStub("key", null),
255 | new ExpirableEntryStub("key", now),
256 | null,
257 | new ExpirableEntryStub("key", now.Add(TimeSpan.FromSeconds(1)))
258 | };
259 |
260 | var comparer = CreateComparer();
261 | var result = array.OrderBy(x => x, comparer).ToArray();
262 |
263 | Assert.Equal(new [] { array[1], array[3], array[0], array[2] }, result);
264 | }
265 |
266 | private ExpirableEntryComparer CreateComparer()
267 | {
268 | return new ExpirableEntryComparer(_stringComparer);
269 | }
270 |
271 | private sealed class ExpirableEntryStub : IExpirableEntry
272 | {
273 | public ExpirableEntryStub(T key, MonotonicTime? expireAt)
274 | {
275 | Key = key;
276 | ExpireAt = expireAt;
277 | }
278 |
279 | public T Key { get; }
280 | public MonotonicTime? ExpireAt { get; set; }
281 | }
282 | }
283 | }
--------------------------------------------------------------------------------
/tests/Hangfire.InMemory.Tests/Entities/JobStateCreatedAtComparerFacts.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.InMemory. Copyright © 2024 Hangfire OÜ.
2 | //
3 | // Hangfire is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU Lesser General Public License as
5 | // published by the Free Software Foundation, either version 3
6 | // of the License, or any later version.
7 | //
8 | // Hangfire is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU Lesser General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU Lesser General Public
14 | // License along with Hangfire. If not, see .
15 |
16 | using System;
17 | using System.Collections.Generic;
18 | using System.Linq;
19 | using Hangfire.Common;
20 | using Hangfire.InMemory.Entities;
21 | using Hangfire.InMemory.State;
22 | using Hangfire.Storage;
23 | using Xunit;
24 |
25 | namespace Hangfire.InMemory.Tests.Entities
26 | {
27 | public class JobStateCreatedAtComparerFacts
28 | {
29 | private readonly StringComparer _stringComparer = StringComparer.Ordinal;
30 | private readonly InvocationData _data = InvocationData.SerializeJob(Job.FromExpression(() => Console.WriteLine()));
31 | private readonly KeyValuePair[] _parameters = [];
32 |
33 | [Fact]
34 | public void Ctor_DoesNotThrowAnException_WhenComparerIsNull()
35 | {
36 | var comparer = new JobStateCreatedAtComparer(null);
37 | Assert.NotNull(comparer);
38 | }
39 |
40 | [Fact]
41 | public void Compare_ReturnsZero_WhenBothEntries_AreNull()
42 | {
43 | var comparer = CreateComparer();
44 |
45 | var result = comparer.Compare(null, null);
46 |
47 | Assert.Equal(0, result);
48 | }
49 |
50 | [Fact]
51 | public void Compare_ReturnsMinusOne_WhenXIsNull_AndYIsNotNull()
52 | {
53 | var comparer = CreateComparer();
54 | var entry = CreateEntry(null, MonotonicTime.GetCurrent(), null, null);
55 |
56 | var result = comparer.Compare(null, entry);
57 |
58 | Assert.Equal(-1, result);
59 | }
60 |
61 | [Fact]
62 | public void Compare_ReturnsPlusOne_WhenXIsNotNull_AndYIsNull()
63 | {
64 | var comparer = CreateComparer();
65 | var entry = CreateEntry(null, MonotonicTime.GetCurrent(), null, null);
66 |
67 | var result = comparer.Compare(entry, null);
68 |
69 | Assert.Equal(+1, result);
70 | }
71 |
72 | [Fact]
73 | public void Compare_ReturnsZero_WhenStateOfBothEntries_IsNull()
74 | {
75 | var comparer = CreateComparer();
76 | var x = CreateEntry(null, MonotonicTime.GetCurrent(), null, null);
77 | var y = CreateEntry(null, MonotonicTime.GetCurrent(), null, null);
78 |
79 | var result = comparer.Compare(x, y);
80 |
81 | Assert.Equal(0, result);
82 | }
83 |
84 | [Fact]
85 | public void Compare_ReturnsMinusOne_WhenStateOfXIsNull_AndStateOfYIsNotNull()
86 | {
87 | var comparer = CreateComparer();
88 | var x = CreateEntry(null, MonotonicTime.GetCurrent(), null, null);
89 | var y = CreateEntry(null, MonotonicTime.GetCurrent(), "State", MonotonicTime.GetCurrent());
90 |
91 | var result = comparer.Compare(x, y);
92 |
93 | Assert.Equal(-1, result);
94 | }
95 |
96 | [Fact]
97 | public void Compare_ReturnsPlusOne_WhenStateOfXIsNotNull_AndStateOfYIsNull()
98 | {
99 | var comparer = CreateComparer();
100 | var x = CreateEntry(null, MonotonicTime.GetCurrent(), "State", MonotonicTime.GetCurrent());
101 | var y = CreateEntry(null, MonotonicTime.GetCurrent(), null, null);
102 |
103 | var result = comparer.Compare(x, y);
104 |
105 | Assert.Equal(+1, result);
106 | }
107 |
108 | [Fact]
109 | public void Compare_LeadsToSorting_InTheAscendingOrder()
110 | {
111 | var now = MonotonicTime.GetCurrent();
112 | var array = new []
113 | {
114 | /* [0]: #6 */ CreateEntry("key", now.Add(TimeSpan.FromDays(1)), "State", now),
115 | /* [1]: #4 */ CreateEntry("key", now, "Another", now),
116 | /* [2]: #5 */ CreateEntry("key", now, "State", now),
117 | /* [3]: #1 */ null,
118 | /* [4]: #2 */ CreateEntry("key", now, null, null),
119 | /* [5]: #7 */ CreateEntry("key", now, "State", now.Add(TimeSpan.FromDays(1))),
120 | /* [6]: #3 */ CreateEntry("another", now, "State", now)
121 | };
122 |
123 | var comparer = CreateComparer();
124 | var result = array.OrderBy(static x => x, comparer).ToArray();
125 |
126 | Assert.Equal([array[3], array[4], array[6], array[1], array[2], array[0], array[5]], result);
127 | }
128 |
129 | private JobEntry CreateEntry(string key, MonotonicTime createdAt, string state, MonotonicTime? stateCreatedAt)
130 | {
131 | var entry = new JobEntry(key, _data, _parameters, createdAt);
132 |
133 | if (state != null)
134 | {
135 | if (stateCreatedAt == null) throw new ArgumentNullException(nameof(stateCreatedAt));
136 | entry.State = new StateRecord(state, null, [], stateCreatedAt.Value);
137 | }
138 |
139 | return entry;
140 | }
141 |
142 | private JobStateCreatedAtComparer CreateComparer()
143 | {
144 | return new JobStateCreatedAtComparer(_stringComparer);
145 | }
146 | }
147 | }
--------------------------------------------------------------------------------
/tests/Hangfire.InMemory.Tests/Entities/LockEntryFacts.cs:
--------------------------------------------------------------------------------
1 | // This file is part of Hangfire.InMemory. Copyright © 2024 Hangfire OÜ.
2 | //
3 | // Hangfire is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU Lesser General Public License as
5 | // published by the Free Software Foundation, either version 3
6 | // of the License, or any later version.
7 | //
8 | // Hangfire is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU Lesser General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU Lesser General Public
14 | // License along with Hangfire. If not, see .
15 |
16 | using System;
17 | using System.Runtime.ExceptionServices;
18 | using System.Threading;
19 | using Hangfire.InMemory.Entities;
20 | using Xunit;
21 |
22 | namespace Hangfire.InMemory.Tests.Entities
23 | {
24 | public class LockEntryFacts
25 | {
26 | private readonly object _owner = new Object();
27 |
28 | [Fact]
29 | public void TryAcquire_ThrowsAnException_WhenOwnerIsNull()
30 | {
31 | var entry = CreateLock();
32 |
33 | var exception = Assert.Throws(
34 | () => entry.TryAcquire(null, TimeSpan.Zero, out _, out _));
35 |
36 | Assert.Equal("owner", exception.ParamName);
37 | }
38 |
39 | [Fact]
40 | public void TryAcquire_AcquiresAnEmptyLock()
41 | {
42 | var entry = CreateLock();
43 |
44 | var acquired = entry.TryAcquire(_owner, TimeSpan.Zero, out var retry, out var cleanUp);
45 |
46 | Assert.True(acquired);
47 | Assert.False(retry);
48 | Assert.False(cleanUp);
49 | }
50 |
51 | [Fact]
52 | public void TryAcquire_AcquiresALock_AlreadyAcquiredByTheSameOwner()
53 | {
54 | var entry = CreateLock();
55 |
56 | entry.TryAcquire(_owner, TimeSpan.Zero, out _, out _);
57 | var acquired = entry.TryAcquire(_owner, TimeSpan.Zero, out var retry, out var cleanUp);
58 |
59 | Assert.True(acquired);
60 | Assert.False(retry);
61 | Assert.False(cleanUp);
62 | }
63 |
64 | [Fact]
65 | public void TryAcquire_DoesNotAcquire_AlreadyOwnedLock()
66 | {
67 | var entry = CreateLock();
68 |
69 | entry.TryAcquire(_owner, TimeSpan.Zero, out _, out _);
70 | var acquired = entry.TryAcquire(new object(), TimeSpan.Zero, out var retry, out var cleanUp);
71 |
72 | Assert.False(acquired);
73 | Assert.False(retry);
74 | Assert.False(cleanUp);
75 | }
76 |
77 | [Fact]
78 | public void TryAcquire_OnAFinalizedLock_RequiresRetry()
79 | {
80 | var entry = CreateLock();
81 | entry.TryAcquire(_owner, TimeSpan.Zero, out _, out _);
82 | entry.Release(_owner, out _);
83 |
84 | var acquired = entry.TryAcquire(_owner, TimeSpan.Zero, out var retry, out var cleanUp);
85 |
86 | Assert.False(acquired);
87 | Assert.True(retry);
88 | Assert.False(cleanUp);
89 | }
90 |
91 | [Fact]
92 | public void Release_ThrowsAnException_WhenOwnerIsNull()
93 | {
94 | var entry = CreateLock();
95 |
96 | var exception = Assert.Throws(
97 | () => entry.Release(null, out _));
98 |
99 | Assert.Equal("owner", exception.ParamName);
100 | }
101 |
102 | [Fact]
103 | public void Release_ThrowsAnException_WhenAttemptedToBeReleasedByAnotherOwner()
104 | {
105 | var entry = CreateLock();
106 | entry.TryAcquire(_owner, TimeSpan.Zero, out _, out _);
107 |
108 | var exception = Assert.Throws(
109 | () => entry.Release(new object(), out _));
110 |
111 | Assert.Equal("owner", exception.ParamName);
112 | }
113 |
114 | [Fact]
115 | public void Release_FinalizesLock_AcquiredByTheSameOwner_WithNoOtherReferences()
116 | {
117 | var entry = CreateLock();
118 |
119 | entry.TryAcquire(_owner, TimeSpan.Zero, out _, out _);
120 | entry.Release(_owner, out var cleanUp);
121 |
122 | Assert.True(cleanUp);
123 | }
124 |
125 | [Fact]
126 | public void Release_DoesNotFinalizeLock_AcquiredMultipleTimes_AndNotFullyReleased()
127 | {
128 | var entry = CreateLock();
129 |
130 | entry.TryAcquire(_owner, TimeSpan.Zero, out _, out _);
131 | entry.TryAcquire(_owner, TimeSpan.Zero, out _, out _);
132 | entry.Release(_owner, out var cleanUp);
133 |
134 | Assert.False(cleanUp);
135 | }
136 |
137 | [Fact]
138 | public void Release_FinalizesLock_AcquiredMultipleTimes_AndFullyReleased()
139 | {
140 | var entry = CreateLock();
141 |
142 | entry.TryAcquire(_owner, TimeSpan.Zero, out _, out _);
143 | entry.TryAcquire(_owner, TimeSpan.Zero, out _, out _);
144 | entry.Release(_owner, out _);
145 | entry.Release(_owner, out var cleanUp);
146 |
147 | Assert.True(cleanUp);
148 | }
149 |
150 | private static LockEntry