├── .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 | [![Latest version](https://img.shields.io/nuget/v/Hangfire.InMemory.svg)](https://www.nuget.org/packages/Hangfire.InMemory/) [![Build status](https://ci.appveyor.com/api/projects/status/yq82w8ji419c61vy?svg=true)](https://ci.appveyor.com/project/HangfireIO/hangfire-inmemory) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=HangfireIO_Hangfire.InMemory&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=HangfireIO_Hangfire.InMemory) [![Bugs](https://sonarcloud.io/api/project_badges/measure?project=HangfireIO_Hangfire.InMemory&metric=bugs)](https://sonarcloud.io/summary/new_code?id=HangfireIO_Hangfire.InMemory) [![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=HangfireIO_Hangfire.InMemory&metric=code_smells)](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 CreateLock() => new LockEntry(); 151 | } 152 | } -------------------------------------------------------------------------------- /tests/Hangfire.InMemory.Tests/Entities/SortedSetItemComparerFacts.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.Linq; 18 | using Hangfire.InMemory.Entities; 19 | using Xunit; 20 | 21 | namespace Hangfire.InMemory.Tests.Entities 22 | { 23 | public class SortedSetItemComparerFacts 24 | { 25 | [Fact] 26 | public void Compare_ReturnsZero_WhenSortingDefaultItems() 27 | { 28 | var comparer = CreateComparer(); 29 | var result = comparer.Compare(default, default); 30 | 31 | Assert.Equal(0, result); 32 | } 33 | 34 | [Fact] 35 | public void Compare_DoesNotTakeValueIntoAccount_WhenItIsNullForX() 36 | { 37 | var comparer = CreateComparer(); 38 | 39 | var result = comparer.Compare(new SortedSetItem(null!, 0.5D), new SortedSetItem("y", 0.5D)); 40 | 41 | Assert.Equal(0, result); 42 | } 43 | 44 | [Fact] 45 | public void Compare_DoesNotTakeValueIntoAccount_WhenItIsNullForY() 46 | { 47 | var comparer = CreateComparer(); 48 | 49 | var result = comparer.Compare(new SortedSetItem("x", 0.5D), new SortedSetItem(null!, 0.5D)); 50 | 51 | Assert.Equal(0, result); 52 | } 53 | 54 | [Fact] 55 | public void Compare_LeadsToSorting_InTheAscendingOrder() 56 | { 57 | var array = new [] 58 | { 59 | /* [0]: #3 */ new SortedSetItem("1", 1.0D), 60 | /* [1]: #2 */ new SortedSetItem("2", 0.5D), 61 | /* [2]: #4 */ new SortedSetItem("2", 1.0D), 62 | /* [3]: #1 */ new SortedSetItem("1", 0.5D) 63 | }; 64 | 65 | var comparer = CreateComparer(); 66 | var result = array.OrderBy(static x => x, comparer).ToArray(); 67 | 68 | Assert.Equal([array[3], array[1], array[0], array[2]], result); 69 | } 70 | 71 | private static SortedSetItemComparer CreateComparer() 72 | { 73 | return new SortedSetItemComparer(StringComparer.Ordinal); 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /tests/Hangfire.InMemory.Tests/GlobalConfigurationExtensionsFacts.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 Xunit; 18 | 19 | namespace Hangfire.InMemory.Tests 20 | { 21 | public class GlobalConfigurationExtensionsFacts 22 | { 23 | private static readonly object SyncRoot = new Object(); 24 | 25 | [Fact] 26 | public void UseInMemoryStorage_SetsTheGlobalJobStorageInstance_WithTheDefaultOptions() 27 | { 28 | lock (SyncRoot) 29 | { 30 | GlobalConfiguration.Configuration.UseInMemoryStorage(); 31 | Assert.IsType(JobStorage.Current); 32 | Assert.NotNull(((InMemoryStorage)JobStorage.Current).Options); 33 | } 34 | } 35 | 36 | [Fact] 37 | public void UseInMemoryStorage_WithOptions_ThrowsAnException_WhenOptionsArgument_IsNull() 38 | { 39 | lock (SyncRoot) 40 | { 41 | var exception = Assert.Throws( 42 | () => GlobalConfiguration.Configuration.UseInMemoryStorage(null)); 43 | 44 | Assert.Equal("options", exception.ParamName); 45 | } 46 | } 47 | 48 | [Fact] 49 | public void UseInMemoryStorage_WithOptions_SetsTheGlobalJobStorageInstance_WithTheGivenOptions() 50 | { 51 | lock (SyncRoot) 52 | { 53 | var options = new InMemoryStorageOptions(); 54 | GlobalConfiguration.Configuration.UseInMemoryStorage(options); 55 | Assert.IsType(JobStorage.Current); 56 | Assert.Same(options, ((InMemoryStorage)JobStorage.Current).Options); 57 | } 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /tests/Hangfire.InMemory.Tests/Hangfire.InMemory.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net452;netcoreapp3.1;net6.0 5 | Latest 6 | $(DefineConstants);HANGFIRE_180 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /tests/Hangfire.InMemory.Tests/ITestServices.cs: -------------------------------------------------------------------------------- 1 | namespace Hangfire.InMemory.Tests 2 | { 3 | public interface ITestServices 4 | { 5 | void Empty(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tests/Hangfire.InMemory.Tests/InMemoryDispatcherBaseFacts.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 26 | { 27 | public class InMemoryDispatcherBaseFacts 28 | { 29 | private readonly MonotonicTime _now; 30 | private readonly InMemoryStorageOptions _options; 31 | private readonly MemoryState _state; 32 | private readonly TestInMemoryDispatcher _dispatcher; 33 | private readonly Dictionary _parameters; 34 | private readonly Job _job; 35 | 36 | public InMemoryDispatcherBaseFacts() 37 | { 38 | _now = MonotonicTime.GetCurrent(); 39 | _options = new InMemoryStorageOptions(); 40 | _state = new MemoryState(_options.StringComparer, _options.StringComparer); 41 | _dispatcher = new TestInMemoryDispatcher(() => _now, _state); 42 | _parameters = new Dictionary(); 43 | _job = Job.FromExpression(x => x.Empty()); 44 | } 45 | 46 | [Fact] 47 | public void Ctor_ThrowsAnException_WhenStateIsNull() 48 | { 49 | var exception = Assert.Throws( 50 | () => new TestInMemoryDispatcher(() => _now, null)); 51 | 52 | Assert.Equal("state", exception.ParamName); 53 | } 54 | 55 | [Fact] 56 | public void Ctor_ThrowsAnException_WhenTimeResolverIsNull() 57 | { 58 | var exception = Assert.Throws( 59 | () => new TestInMemoryDispatcher(null, _state)); 60 | 61 | Assert.Equal("timeResolver", exception.ParamName); 62 | } 63 | 64 | [Fact] 65 | public void EvictEntries_EvictsExpiredJobs() 66 | { 67 | // Arrange 68 | _state.JobCreate(CreateJobEntry("job-1"), expireIn: TimeSpan.FromSeconds(-1)); 69 | _state.JobCreate(CreateJobEntry("job-2"), expireIn: TimeSpan.FromMinutes(-1)); 70 | _state.JobCreate(CreateJobEntry("job-3"), expireIn: TimeSpan.FromHours(-1)); 71 | 72 | // Act 73 | _dispatcher.EvictExpiredEntries(); 74 | 75 | // Assert 76 | Assert.Empty(_state.Jobs); 77 | } 78 | 79 | [Fact] 80 | public void EvictEntries_EvictsExpiredHashes() 81 | { 82 | // Arrange 83 | _state.HashExpire(_state.HashGetOrAdd("hash-1"), _now, expireIn: TimeSpan.FromSeconds(-1), _options.MaxExpirationTime); 84 | _state.HashExpire(_state.HashGetOrAdd("hash-2"), _now, expireIn: TimeSpan.FromMinutes(-1), _options.MaxExpirationTime); 85 | _state.HashExpire(_state.HashGetOrAdd("hash-3"), _now, expireIn: TimeSpan.FromHours(-1), _options.MaxExpirationTime); 86 | 87 | // Act 88 | _dispatcher.EvictExpiredEntries(); 89 | 90 | // Assert 91 | Assert.Empty(_state.Hashes); 92 | } 93 | 94 | [Fact] 95 | public void EvictEntries_EvictsExpiredSets() 96 | { 97 | // Arrange 98 | _state.SetExpire(_state.SetGetOrAdd("set-1"), _now, expireIn: TimeSpan.FromSeconds(-1), _options.MaxExpirationTime); 99 | _state.SetExpire(_state.SetGetOrAdd("set-2"), _now, expireIn: TimeSpan.FromMinutes(-1), _options.MaxExpirationTime); 100 | _state.SetExpire(_state.SetGetOrAdd("set-3"), _now, expireIn: TimeSpan.FromHours(-1), _options.MaxExpirationTime); 101 | 102 | // Act 103 | _dispatcher.EvictExpiredEntries(); 104 | 105 | // Assert 106 | Assert.Empty(_state.Sets); 107 | } 108 | 109 | [Fact] 110 | public void EvictEntries_EvictsExpiredLists() 111 | { 112 | // Arrange 113 | _state.ListExpire(_state.ListGetOrAdd("list-1"), _now, expireIn: TimeSpan.FromSeconds(-1), _options.MaxExpirationTime); 114 | _state.ListExpire(_state.ListGetOrAdd("list-2"), _now, expireIn: TimeSpan.FromMinutes(-1), _options.MaxExpirationTime); 115 | _state.ListExpire(_state.ListGetOrAdd("list-3"), _now, expireIn: TimeSpan.FromHours(-1), _options.MaxExpirationTime); 116 | 117 | // Act 118 | _dispatcher.EvictExpiredEntries(); 119 | 120 | // Assert 121 | Assert.Empty(_state.Lists); 122 | } 123 | 124 | [Fact] 125 | public void EvictEntries_EvictsExpiredCounters() 126 | { 127 | // Arrange 128 | _state.CounterExpire(_state.CounterGetOrAdd("counter-1"), _now, expireIn: TimeSpan.FromSeconds(-1)); 129 | _state.CounterExpire(_state.CounterGetOrAdd("counter-2"), _now, expireIn: TimeSpan.FromMinutes(-1)); 130 | _state.CounterExpire(_state.CounterGetOrAdd("counter-3"), _now, expireIn: TimeSpan.FromHours(-1)); 131 | 132 | // Act 133 | _dispatcher.EvictExpiredEntries(); 134 | 135 | // Assert 136 | Assert.Empty(_state.Counters); 137 | } 138 | 139 | [Fact] 140 | public void EvictEntries_DoesNotEvict_NonExpiringEntries() 141 | { 142 | // Arrange 143 | _state.JobCreate(CreateJobEntry("my-job"), expireIn: null); 144 | _state.HashExpire(_state.HashGetOrAdd("my-hash"), _now, expireIn: null, _options.MaxExpirationTime); 145 | _state.SetExpire(_state.SetGetOrAdd("my-set"), _now, expireIn: null, _options.MaxExpirationTime); 146 | _state.ListExpire(_state.ListGetOrAdd("my-list"), _now, expireIn: null, _options.MaxExpirationTime); 147 | _state.CounterExpire(_state.CounterGetOrAdd("my-counter"), _now, expireIn: null); 148 | 149 | // Act 150 | _dispatcher.EvictExpiredEntries(); 151 | 152 | // Assert 153 | Assert.Contains("my-job", _state.Jobs.Keys); 154 | Assert.Contains("my-hash", _state.Hashes.Keys); 155 | Assert.Contains("my-set", _state.Sets.Keys); 156 | Assert.Contains("my-list", _state.Lists.Keys); 157 | Assert.Contains("my-counter", _state.Counters.Keys); 158 | } 159 | 160 | [Fact] 161 | public void EvictEntries_DoesNotEvict_StillExpiringEntries() 162 | { 163 | // Arrange 164 | _state.JobCreate(CreateJobEntry("my-job"), expireIn: TimeSpan.FromMinutes(30)); 165 | _state.HashExpire(_state.HashGetOrAdd("my-hash"), _now, expireIn: TimeSpan.FromMinutes(30), _options.MaxExpirationTime); 166 | _state.SetExpire(_state.SetGetOrAdd("my-set"), _now, expireIn: TimeSpan.FromMinutes(30), _options.MaxExpirationTime); 167 | _state.ListExpire(_state.ListGetOrAdd("my-list"), _now, expireIn: TimeSpan.FromMinutes(30), _options.MaxExpirationTime); 168 | _state.CounterExpire(_state.CounterGetOrAdd("my-counter"), _now, expireIn: TimeSpan.FromMinutes(30)); 169 | 170 | // Act 171 | _dispatcher.EvictExpiredEntries(); 172 | 173 | // Assert 174 | Assert.Contains("my-job", _state.Jobs.Keys); 175 | Assert.Contains("my-hash", _state.Hashes.Keys); 176 | Assert.Contains("my-set", _state.Sets.Keys); 177 | Assert.Contains("my-list", _state.Lists.Keys); 178 | Assert.Contains("my-counter", _state.Counters.Keys); 179 | } 180 | 181 | [Fact] 182 | public void EvictEntries_DoesStumbleUpon_NonExpiredEntries() 183 | { 184 | // Arrange 185 | _state.JobCreate(CreateJobEntry("job-0"), expireIn: TimeSpan.Zero); 186 | _state.JobCreate(CreateJobEntry("job-1"), expireIn: null); 187 | _state.JobCreate(CreateJobEntry("job-2"), expireIn: TimeSpan.FromMinutes(30)); 188 | _state.JobCreate(CreateJobEntry("job-3"), expireIn: TimeSpan.FromMinutes(-30)); 189 | _state.HashExpire(_state.HashGetOrAdd("hash-0"), _now, expireIn: TimeSpan.Zero, _options.MaxExpirationTime); 190 | _state.HashExpire(_state.HashGetOrAdd("hash-1"), _now, expireIn: null, _options.MaxExpirationTime); 191 | _state.HashExpire(_state.HashGetOrAdd("hash-2"), _now, expireIn: TimeSpan.FromMinutes(30), _options.MaxExpirationTime); 192 | _state.HashExpire(_state.HashGetOrAdd("hash-3"), _now, expireIn: TimeSpan.FromMinutes(-30), _options.MaxExpirationTime); 193 | _state.SetExpire(_state.SetGetOrAdd("set-0"), _now, expireIn: TimeSpan.Zero, _options.MaxExpirationTime); 194 | _state.SetExpire(_state.SetGetOrAdd("set-1"), _now, expireIn: null, _options.MaxExpirationTime); 195 | _state.SetExpire(_state.SetGetOrAdd("set-2"), _now, expireIn: TimeSpan.FromMinutes(30), _options.MaxExpirationTime); 196 | _state.SetExpire(_state.SetGetOrAdd("set-3"), _now, expireIn: TimeSpan.FromMinutes(-30), _options.MaxExpirationTime); 197 | _state.ListExpire(_state.ListGetOrAdd("list-0"), _now, expireIn: TimeSpan.Zero, _options.MaxExpirationTime); 198 | _state.ListExpire(_state.ListGetOrAdd("list-1"), _now, expireIn: null, _options.MaxExpirationTime); 199 | _state.ListExpire(_state.ListGetOrAdd("list-2"), _now, expireIn: TimeSpan.FromMinutes(30), _options.MaxExpirationTime); 200 | _state.ListExpire(_state.ListGetOrAdd("list-3"), _now, expireIn: TimeSpan.FromMinutes(-30), _options.MaxExpirationTime); 201 | _state.CounterExpire(_state.CounterGetOrAdd("counter-0"), _now, expireIn: TimeSpan.Zero); 202 | _state.CounterExpire(_state.CounterGetOrAdd("counter-1"), _now, expireIn: null); 203 | _state.CounterExpire(_state.CounterGetOrAdd("counter-2"), _now, expireIn: TimeSpan.FromMinutes(30)); 204 | _state.CounterExpire(_state.CounterGetOrAdd("counter-3"), _now, expireIn: TimeSpan.FromMinutes(-30)); 205 | 206 | // Act 207 | _dispatcher.EvictExpiredEntries(); 208 | 209 | // Assert 210 | Assert.Equal(new [] { "job-1", "job-2" }, _state.Jobs.Keys.OrderBy(x => x)); 211 | Assert.Equal(new [] { "hash-1", "hash-2" }, _state.Hashes.Keys.OrderBy(x => x)); 212 | Assert.Equal(new [] { "set-1", "set-2" }, _state.Sets.Keys.OrderBy(x => x)); 213 | Assert.Equal(new [] { "list-1", "list-2" }, _state.Lists.Keys.OrderBy(x => x)); 214 | Assert.Equal(new [] { "counter-1", "counter-2" }, _state.Counters.Keys.OrderBy(x => x)); 215 | } 216 | 217 | private JobEntry CreateJobEntry(string jobId) 218 | { 219 | return new JobEntry(jobId, InvocationData.SerializeJob(_job), _parameters.ToArray(), _now); 220 | } 221 | } 222 | } -------------------------------------------------------------------------------- /tests/Hangfire.InMemory.Tests/InMemoryFetchedJobFacts.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.State; 19 | using Hangfire.Storage; 20 | using Xunit; 21 | 22 | // ReSharper disable AssignNullToNotNullAttribute 23 | // ReSharper disable PossibleNullReferenceException 24 | 25 | namespace Hangfire.InMemory.Tests 26 | { 27 | public class InMemoryFetchedJobFacts 28 | { 29 | private readonly MemoryState _state; 30 | private readonly InMemoryConnection _connection; 31 | 32 | public InMemoryFetchedJobFacts() 33 | { 34 | var now = MonotonicTime.GetCurrent(); 35 | var options = new InMemoryStorageOptions(); 36 | _state = new MemoryState(options.StringComparer, options.StringComparer); 37 | 38 | var dispatcher = new TestInMemoryDispatcher(() => now, _state); 39 | var keyProvider = new StringKeyProvider(); 40 | _connection = new InMemoryConnection(options, dispatcher, keyProvider); 41 | } 42 | 43 | [Fact] 44 | public void Ctor_ThrowsAnException_WhenConnectionIsNull() 45 | { 46 | var exception = Assert.Throws( 47 | () => new InMemoryFetchedJob(null, "default", "123")); 48 | 49 | Assert.Equal("connection", exception.ParamName); 50 | } 51 | 52 | [Fact] 53 | public void Ctor_ThrowsAnException_WhenQueueIsNull() 54 | { 55 | var exception = Assert.Throws( 56 | () => new InMemoryFetchedJob(_connection, null, "123")); 57 | 58 | Assert.Equal("queueName", exception.ParamName); 59 | } 60 | 61 | [Fact] 62 | public void Ctor_ThrowsAnException_WhenJobIdIsNull() 63 | { 64 | var exception = Assert.Throws( 65 | () => new InMemoryFetchedJob(_connection, "default", null)); 66 | 67 | Assert.Equal("jobId", exception.ParamName); 68 | } 69 | 70 | [Fact] 71 | public void Ctor_CorrectlySets_AllTheProperties() 72 | { 73 | var fetched = new InMemoryFetchedJob(_connection, "critical", "12345"); 74 | 75 | Assert.Equal("critical", fetched.QueueName); 76 | Assert.Equal("12345", fetched.JobId); 77 | } 78 | 79 | [Fact] 80 | public void Requeue_EnqueuesTheGivenJobId_ToTheGivenQueue() 81 | { 82 | var fetched = new InMemoryFetchedJob(_connection, "critical", "12345"); 83 | 84 | fetched.Requeue(); 85 | 86 | Assert.Equal("12345", _state.Queues["critical"].Queue.Single()); 87 | } 88 | 89 | [Fact] 90 | public void RemoveFromQueue_DoesNotDoAnything() 91 | { 92 | IFetchedJob fetched = new InMemoryFetchedJob(_connection, "critical", "12345"); 93 | fetched.RemoveFromQueue(); 94 | } 95 | 96 | [Fact] 97 | public void Dispose_DoesNotDoAnything() 98 | { 99 | IFetchedJob fetched = new InMemoryFetchedJob(_connection, "critical", "12345"); 100 | fetched.Dispose(); 101 | } 102 | } 103 | } -------------------------------------------------------------------------------- /tests/Hangfire.InMemory.Tests/InMemoryStorageFacts.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 Xunit; 19 | 20 | // ReSharper disable AssignNullToNotNullAttribute 21 | // ReSharper disable PossibleNullReferenceException 22 | 23 | namespace Hangfire.InMemory.Tests 24 | { 25 | public class InMemoryStorageFacts 26 | { 27 | [Fact] 28 | public void LinearizableRead_Property_ReturnsTrue() 29 | { 30 | // Arrange 31 | using var storage = CreateStorage(); 32 | 33 | // Act & Assert 34 | Assert.True(storage.LinearizableReads); 35 | } 36 | 37 | #if !HANGFIRE_170 38 | [Fact] 39 | public void HasFeature_ThrowsArgumentNullException_WhenFeatureIdIsNull() 40 | { 41 | // Arrange 42 | using var storage = CreateStorage(); 43 | 44 | // Act 45 | var exception = Assert.Throws( 46 | () => storage.HasFeature(null)); 47 | 48 | // Assert 49 | Assert.Equal("featureId", exception.ParamName); 50 | } 51 | 52 | [Fact] 53 | public void HasFeature_ReturnsTrue_ForTheFollowingFeatures() 54 | { 55 | // Arrange 56 | using var storage = CreateStorage(); 57 | 58 | // Act & Assert 59 | Assert.True(storage.HasFeature("Storage.ExtendedApi")); 60 | Assert.True(storage.HasFeature("Job.Queue")); 61 | Assert.True(storage.HasFeature("Connection.BatchedGetFirstByLowestScoreFromSet")); 62 | Assert.True(storage.HasFeature("Connection.GetUtcDateTime")); 63 | Assert.True(storage.HasFeature("Connection.GetSetContains")); 64 | Assert.True(storage.HasFeature("Connection.GetSetCount.Limited")); 65 | Assert.True(storage.HasFeature("Transaction.AcquireDistributedLock")); 66 | Assert.True(storage.HasFeature("Transaction.CreateJob")); 67 | Assert.True(storage.HasFeature("Transaction.SetJobParameter")); 68 | Assert.True(storage.HasFeature("TransactionalAcknowledge:InMemoryFetchedJob")); 69 | Assert.True(storage.HasFeature("Monitoring.DeletedStateGraphs")); 70 | Assert.True(storage.HasFeature("Monitoring.AwaitingJobs")); 71 | Assert.False(storage.HasFeature("SomeNonExistingFeature")); 72 | } 73 | #endif 74 | 75 | [Fact] 76 | public void Dispose_ReturnsAlmostImmediately() 77 | { 78 | var storage = CreateStorage(); 79 | var sw = Stopwatch.StartNew(); 80 | 81 | storage.Dispose(); 82 | 83 | sw.Stop(); 84 | Assert.True(sw.ElapsedMilliseconds < 1_000); 85 | } 86 | 87 | [Fact] 88 | public void GetConnection_ReturnsUsableInstance() 89 | { 90 | // Arrange 91 | using var storage = CreateStorage(); 92 | 93 | // Act 94 | using var connection = storage.GetConnection(); 95 | 96 | // Assert 97 | Assert.NotNull(connection); 98 | } 99 | 100 | [Fact] 101 | public void GetMonitoringApi_ReturnsUsableInstance() 102 | { 103 | // Arrange 104 | using var storage = CreateStorage(); 105 | 106 | // Act 107 | var monitoringApi = storage.GetMonitoringApi(); 108 | 109 | // Assert 110 | Assert.NotNull(monitoringApi); 111 | } 112 | 113 | [Fact] 114 | public void ToString_ReturnsUsefulString() 115 | { 116 | // Arrange 117 | using var storage = CreateStorage(); 118 | 119 | // Act 120 | var result = storage.ToString(); 121 | 122 | // Assert 123 | Assert.Equal("In-Memory Storage", result); 124 | } 125 | 126 | private static InMemoryStorage CreateStorage() 127 | { 128 | return new InMemoryStorage(); 129 | } 130 | } 131 | } -------------------------------------------------------------------------------- /tests/Hangfire.InMemory.Tests/MonotonicTimeFacts.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 | using System.Threading; 19 | using Hangfire.InMemory.State; 20 | using Xunit; 21 | 22 | namespace Hangfire.InMemory.Tests 23 | { 24 | public class MonotonicTimeFacts 25 | { 26 | [Fact] 27 | public void GetCurrent_ReturnsBiggerValues_ForSubsequentCalls() 28 | { 29 | var time1 = MonotonicTime.GetCurrent(); 30 | Thread.Sleep(10); 31 | var time2 = MonotonicTime.GetCurrent(); 32 | Thread.Sleep(10); 33 | var time3 = MonotonicTime.GetCurrent(); 34 | 35 | Assert.True(time1 < time2 && time2 < time3); 36 | } 37 | 38 | [Fact] 39 | public void ToUtcDateTime_ReturnsCorrectValue() 40 | { 41 | var utcTime = MonotonicTime.GetCurrent().ToUtcDateTime(); 42 | var utcNow = DateTime.UtcNow; 43 | 44 | AssertWithinSecond(utcTime, utcNow); 45 | } 46 | 47 | [Fact] 48 | public void Add_CorrectlyAdds_TheGivenPositiveTimeSpan() 49 | { 50 | var utcTime = MonotonicTime.GetCurrent().ToUtcDateTime().Add(TimeSpan.FromDays(1)); 51 | var utcNow = DateTime.UtcNow.AddDays(1); 52 | 53 | AssertWithinSecond(utcTime, utcNow); 54 | } 55 | 56 | [Fact] 57 | public void Add_CorrectlyAdds_TheGivenNegativeTimeSpan() 58 | { 59 | var utcTime = MonotonicTime.GetCurrent().ToUtcDateTime().Add(TimeSpan.FromMinutes(-1)); 60 | var utcNow = DateTime.UtcNow.AddMinutes(-1); 61 | 62 | AssertWithinSecond(utcTime, utcNow); 63 | } 64 | 65 | [Fact] 66 | public void Add_CanHandle_BigPositiveValues() 67 | { 68 | var utcTime = MonotonicTime.GetCurrent().ToUtcDateTime().Add(TimeSpan.FromDays(10000)); 69 | var utcNow = DateTime.UtcNow.AddDays(10000); 70 | 71 | AssertWithinSecond(utcTime, utcNow); 72 | } 73 | 74 | [Fact] 75 | public void Add_CanHandle_BigNegativeValues() 76 | { 77 | var utcTime = MonotonicTime.GetCurrent().ToUtcDateTime().Add(TimeSpan.FromDays(-10000)); 78 | var utcNow = DateTime.UtcNow.AddDays(-10000); 79 | 80 | AssertWithinSecond(utcTime, utcNow); 81 | } 82 | 83 | [Fact] 84 | public void Equals_ReturnsTrue_OnEqualValues() 85 | { 86 | var time1 = MonotonicTime.GetCurrent(); 87 | var time2 = time1; 88 | 89 | Assert.Equal(time1, time2); 90 | Assert.True(time1.Equals(time2)); 91 | Assert.True(time1.Equals((object)time2)); 92 | Assert.True(time1 == time2); 93 | Assert.False(time1 != time2); 94 | } 95 | 96 | [Fact] 97 | public void Equals_ReturnsTrue_OnDifferentValues() 98 | { 99 | var time1 = MonotonicTime.GetCurrent(); 100 | var time2 = time1.Add(TimeSpan.FromSeconds(1)); 101 | 102 | Assert.NotEqual(time1, time2); 103 | Assert.False(time1.Equals(time2)); 104 | Assert.False(time1.Equals((object)time2)); 105 | Assert.False(time1 == time2); 106 | Assert.True(time1 != time2); 107 | } 108 | 109 | [Fact] 110 | public void Equals_Object_ReturnsFalse_WhenNullValueIsGiven() 111 | { 112 | var time = MonotonicTime.GetCurrent(); 113 | Assert.False(time.Equals(null)); 114 | } 115 | 116 | [Fact] 117 | public void Equals_Object_ReturnsFalse_ForOtherTypes() 118 | { 119 | var time = MonotonicTime.GetCurrent(); 120 | // ReSharper disable SuspiciousTypeConversion.Global 121 | Assert.False(time.Equals(12345)); 122 | // ReSharper restore SuspiciousTypeConversion.Global 123 | } 124 | 125 | [Fact] 126 | public void GetHashCode_ReturnsEqualValues_ForEqualInstances() 127 | { 128 | var time = MonotonicTime.GetCurrent(); 129 | Assert.Equal(time.GetHashCode(), time.GetHashCode()); 130 | } 131 | 132 | [Fact] 133 | public void GetHashCode_ReturnsDifferentValues_ForDifferentInstances() 134 | { 135 | var time1 = MonotonicTime.GetCurrent(); 136 | var time2 = time1.Add(TimeSpan.FromSeconds(1)); 137 | 138 | Assert.NotEqual(time1.GetHashCode(), time2.GetHashCode()); 139 | } 140 | 141 | [Fact] 142 | public void CompareTo_ReturnsZero_OnEqualComparisons() 143 | { 144 | var time1 = MonotonicTime.GetCurrent(); 145 | var time2 = time1; 146 | 147 | Assert.Equal(0, time1.CompareTo(time2)); 148 | Assert.Equal(0, time1.CompareTo((object)time2)); 149 | Assert.True(time1 >= time2); 150 | Assert.True(time1 <= time2); 151 | Assert.False(time1 < time2); 152 | Assert.False(time1 > time2); 153 | } 154 | 155 | [Fact] 156 | public void CompareTo_ReturnsOne_WhenValueIsBigger_ThanTheGivenOne() 157 | { 158 | var time1 = MonotonicTime.GetCurrent(); 159 | var time2 = time1.Add(TimeSpan.FromSeconds(-1)); 160 | 161 | Assert.Equal(1, time1.CompareTo(time2)); 162 | Assert.Equal(1, time1.CompareTo((object)time2)); 163 | Assert.True(time1 >= time2); 164 | Assert.True(time1 > time2); 165 | Assert.False(time1 <= time2); 166 | Assert.False(time1 < time2); 167 | } 168 | 169 | [Fact] 170 | public void CompareTo_ReturnsMinusOne_WhenValueIsSmaller_ThanTheGivenOne() 171 | { 172 | var time1 = MonotonicTime.GetCurrent(); 173 | var time2 = time1.Add(TimeSpan.FromSeconds(1)); 174 | 175 | Assert.Equal(-1, time1.CompareTo(time2)); 176 | Assert.Equal(-1, time1.CompareTo((object)time2)); 177 | Assert.False(time1 >= time2); 178 | Assert.False(time1 > time2); 179 | Assert.True(time1 <= time2); 180 | Assert.True(time1 < time2); 181 | } 182 | 183 | [Fact] 184 | public void CompareTo_Object_ReturnsOne_WhenNullValueIsGiven() 185 | { 186 | var time = MonotonicTime.GetCurrent(); 187 | Assert.Equal(1, time.CompareTo(null)); 188 | } 189 | 190 | [Fact] 191 | public void CompareTo_Object_ThrowsAnException_ForOtherTypes() 192 | { 193 | var time = MonotonicTime.GetCurrent(); 194 | var exception = Assert.Throws( 195 | () => time.CompareTo(1234)); 196 | 197 | Assert.Equal("obj", exception.ParamName); 198 | } 199 | 200 | [Fact] 201 | public void ToString_ReturnsSomeValue() 202 | { 203 | var result = MonotonicTime.GetCurrent().ToString(); 204 | Assert.NotNull(result); 205 | } 206 | 207 | [Fact] 208 | [SuppressMessage("SonarLint", "S1764:IdenticalExpressionsShouldNotBeUsedOnBothSidesOfOperators", Justification = "Checking the correct behavior.")] 209 | public void op_Subtraction_ReturnsZeroTimeSpan_ForEqualValues() 210 | { 211 | var time = MonotonicTime.GetCurrent(); 212 | Assert.Equal(TimeSpan.Zero, time - time); 213 | } 214 | 215 | [Fact] 216 | public void op_Subtraction_ReturnsCorrectPositiveTimeSpan_WhenSubtractingFromBiggerValue() 217 | { 218 | var span = TimeSpan.FromDays(1); 219 | var time = MonotonicTime.GetCurrent(); 220 | var nextDay = time.Add(span); 221 | 222 | Assert.Equal(span, nextDay - time); 223 | } 224 | 225 | [Fact] 226 | public void op_Subtraction_ReturnsCorrectNegativeTimeSpan_WhenSubtractingFromLowerValue() 227 | { 228 | var span = TimeSpan.FromHours(1); 229 | var time = MonotonicTime.GetCurrent(); 230 | var nextHour = time.Add(span); 231 | 232 | Assert.Equal(span.Negate(), time - nextHour); 233 | } 234 | 235 | private static void AssertWithinSecond(DateTime date1, DateTime date2) 236 | { 237 | Assert.Equal(0, (date1 - date2).TotalSeconds, 1); 238 | } 239 | } 240 | } -------------------------------------------------------------------------------- /tests/Hangfire.InMemory.Tests/State/DispatcherFacts.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.CompilerServices; 18 | using Hangfire.InMemory.State; 19 | using Xunit; 20 | 21 | namespace Hangfire.InMemory.Tests.State 22 | { 23 | public class DispatcherFacts 24 | { 25 | private readonly MemoryState _state; 26 | private readonly MonotonicTime _now; 27 | private readonly Func _timeResolver; 28 | 29 | public DispatcherFacts() 30 | { 31 | var options = new InMemoryStorageOptions(); 32 | _now = MonotonicTime.GetCurrent(); 33 | _state = new MemoryState(options.StringComparer, options.StringComparer); 34 | _timeResolver = () => _now; 35 | } 36 | 37 | [Fact] 38 | public void Ctor_ThrowsAnException_WhenThreadNameIsNull() 39 | { 40 | var exception = Assert.Throws( 41 | () => new Dispatcher(null!, _timeResolver, _state)); 42 | 43 | Assert.Equal("threadName", exception.ParamName); 44 | } 45 | 46 | [Fact] 47 | public void Dispose_DisposesTheDispatcherInstance_WithoutAnyException() 48 | { 49 | using (CreateDispatcher()) 50 | { 51 | } 52 | } 53 | 54 | [Fact] 55 | public void QueryWriteAndWait_IsBeingEventuallyExecuted() 56 | { 57 | using var dispatcher = CreateDispatcher(); 58 | var box = new StrongBox(); 59 | 60 | var result = dispatcher.QueryWriteAndWait(box, static (b, _) => b.Value = true); 61 | 62 | Assert.True(box.Value); 63 | Assert.True(result); 64 | } 65 | 66 | [Fact] 67 | public void QueryReadAndWait_IsBeingEventuallyExecuted() 68 | { 69 | using var dispatcher = CreateDispatcher(); 70 | 71 | var result = dispatcher.QueryReadAndWait(false, static (_, _) => true); 72 | 73 | Assert.True(result); 74 | } 75 | 76 | private Dispatcher CreateDispatcher() 77 | { 78 | return new Dispatcher("DispatcherThread", _timeResolver, _state); 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /tests/Hangfire.InMemory.Tests/StringKeyProvider.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.Tests 19 | { 20 | internal sealed class StringKeyProvider : IKeyProvider 21 | { 22 | public string GetUniqueKey() 23 | { 24 | return Guid.NewGuid().ToString("D"); 25 | } 26 | 27 | public bool TryParse(string input, out string key) 28 | { 29 | key = input; 30 | return true; 31 | } 32 | 33 | public string ToString(string key) 34 | { 35 | return key; 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /tests/Hangfire.InMemory.Tests/TestInMemoryDispatcher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Hangfire.InMemory.State; 3 | 4 | namespace Hangfire.InMemory.Tests 5 | { 6 | internal sealed class TestInMemoryDispatcher : DispatcherBase 7 | where TKey : IComparable 8 | { 9 | public TestInMemoryDispatcher(Func timeResolver, MemoryState state) : base(timeResolver, state) 10 | { 11 | } 12 | 13 | public new void EvictExpiredEntries() 14 | { 15 | base.EvictExpiredEntries(); 16 | } 17 | } 18 | } --------------------------------------------------------------------------------