├── .github └── workflows │ ├── build.yml │ └── publish.yml ├── .gitignore ├── DistributedScheme.png ├── LICENSE ├── README.md └── src ├── Horarium.AspNetCore ├── Horarium.AspNetCore.csproj ├── HorariumLogger.cs ├── HorariumServerHostedService.cs ├── JobScope.cs ├── JobScopeFactory.cs └── RegistrationHorariumExtension.cs ├── Horarium.InMemory ├── Horarium.InMemory.csproj ├── InMemoryRepository.cs ├── Indexes │ ├── Comparers │ │ ├── JobIdComparer.cs │ │ ├── JobKeyComparer.cs │ │ ├── StartAtComparer.cs │ │ ├── StartedExecutingComparer.cs │ │ └── StaticJobIdComparer.cs │ ├── ExecutingJobIndex.cs │ ├── FailedJobIndex.cs │ ├── IAddRemoveIndex.cs │ ├── JobKeyIndex.cs │ ├── ReadyJobIndex.cs │ └── RepeatJobIndex.cs ├── JobDbExtension.cs ├── JobsStorage.cs └── OperationsProcessor.cs ├── Horarium.IntegrationTest ├── FallbackJobTest.cs ├── Horarium.IntegrationTest.csproj ├── IntegrationTestBase.cs ├── Jobs │ ├── Fallback │ │ ├── FallbackJob.cs │ │ ├── FallbackMainJob.cs │ │ ├── FallbackNextJob.cs │ │ └── FallbackRepeatStrategy.cs │ ├── OneTimeJob.cs │ ├── RecurrentJob.cs │ ├── RecurrentJobForUpdate.cs │ ├── RepeatFailedJob.cs │ ├── SequenceJob.cs │ ├── TestJob.cs │ ├── TestJobParam.cs │ └── TestObsoleteJob.cs ├── OneTimeJobTest.cs ├── RecurrentJobTest.cs ├── RepeatFailedJobTest.cs ├── SequenceJobTest.cs └── TestParallelsWorkTwoManagers.cs ├── Horarium.Mongo ├── Horarium.Mongo.csproj ├── IMongoClientProvider.cs ├── JobMongoModel.cs ├── MongoClientProvider.cs ├── MongoEntityAttribute.cs ├── MongoRepository.cs ├── MongoRepositoryFactory.cs └── RecurrentJobSettingsMongo.cs ├── Horarium.Sample ├── CustomRepeatStrategy.cs ├── FailedTestJob.cs ├── FallbackTestJob.cs ├── Horarium.Sample.csproj ├── Program.cs ├── TestJob.cs └── TestRecurrentJob.cs ├── Horarium.Test ├── AdderJobTest.cs ├── AspNetCore │ └── RegistrationHorariumExtensionTest.cs ├── Builders │ ├── ParameterizedJobBuilderTest.cs │ └── RecurrentJobBuilderTest.cs ├── ExecutorJobTest.cs ├── Horarium.Test.csproj ├── JobMapperTest.cs ├── Mongo │ ├── JobMongoModelMapperTest.cs │ └── MongoRepositoryFactoryTest.cs ├── RunnerJobTest.cs ├── SchedulerSettingsTest.cs ├── TestJob.cs ├── TestRecurrentJob.cs └── UncompletedTaskListTests.cs ├── Horarium.sln └── Horarium ├── Builders ├── IDelayedJobBuilder.cs ├── IJobBuilder.cs ├── JobBuilder.cs ├── JobBuilderHelpers.cs ├── JobSequenceBuilder │ ├── IJobSequenceBuilder.cs │ └── JobSequenceBuilder.cs ├── Parameterized │ ├── IParameterizedJobBuilder.cs │ └── ParameterizedJobBuilder.cs └── Recurrent │ ├── IRecurrentJobBuilder.cs │ └── RecurrentJobBuilder.cs ├── Cron.cs ├── DefaultJobFactory.cs ├── DefaultRepeatStrategy.cs ├── EmptyLogger.cs ├── Fallbacks ├── FallbackStrategyOptions.cs ├── FallbackStrategyTypeEnum.cs └── IFallbackStrategyOptions.cs ├── Handlers ├── AdderJobs.cs ├── ExecutorJob.cs ├── RecurrentJobSettingsAdder.cs ├── RunnerJobs.cs ├── StatisticsJobs.cs └── UncompletedTaskList.cs ├── Horarium.csproj ├── HorariumClient.cs ├── HorariumServer.cs ├── HorariumSettings.cs ├── Interfaces ├── IAdderJobs.cs ├── IAllRepeatesIsFailed.cs ├── IExecutorJob.cs ├── IFailedRepeatStrategy.cs ├── IHorarium.cs ├── IHorariumLogger.cs ├── IJob.cs ├── IJobScope.cs ├── IJobScopeFactory.cs ├── IRecurrentJobSettingsAdder.cs ├── IRunnerJobs.cs ├── ISequenceJobs.cs ├── IStatisticsJobs.cs └── IUncompletedTaskList.cs ├── JobMetadata.cs ├── JobStatus.cs ├── JobThrottleSettings.cs ├── RecurrentJobSettingsMetadata.cs ├── Repository ├── IJobRepository.cs ├── JobDb.cs └── RecurrentJobSettings.cs └── Utils.cs /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | workflow_call: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | services: 12 | mongodb: 13 | image: mongo:5.0.10 14 | options: >- 15 | --health-cmd "echo 'db.runCommand("ping").ok' | mongo --host localhost --quiet --eval 'db.version()'" 16 | --health-interval 10s 17 | --health-timeout 5s 18 | --health-retries 5 19 | ports: 20 | - 27017:27017 21 | steps: 22 | - uses: actions/checkout@v3 23 | - uses: actions/setup-dotnet@v2 24 | with: 25 | dotnet-version: '2.2.x' # SDK Version to use. 26 | env: 27 | MONGO_ADDRESS: localhost 28 | - name: build 29 | run: | 30 | dotnet build src/Horarium.sln -c Release 31 | - name: run unit tests 32 | run: | 33 | dotnet test src/Horarium.Test/Horarium.Test.csproj -c Release --no-restore 34 | - name: run MongoDB integration tests 35 | run: | 36 | DataBase=MongoDB dotnet test src/Horarium.IntegrationTest/Horarium.IntegrationTest.csproj -c Release --no-restore 37 | - name: run InMemory integration tests 38 | run: | 39 | DataBase=Memory dotnet test src/Horarium.IntegrationTest/Horarium.IntegrationTest.csproj -c Release --no-restore 40 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | release: 5 | types: [released] 6 | 7 | jobs: 8 | build: 9 | uses: ./.github/workflows/build.yml 10 | 11 | deploy: 12 | needs: [ build ] 13 | runs-on: ubuntu-latest 14 | env: 15 | VERSION: ${{ github.event.release.tag_name }} 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: actions/setup-dotnet@v2 19 | with: 20 | dotnet-version: '2.2.x' # SDK Version to use. 21 | - name: prepare version 22 | run: | 23 | echo "$VERSION" 24 | - name: pack Horarium 25 | run: | 26 | cd src 27 | dotnet pack Horarium/Horarium.csproj -c Release /p:PackageVersion=${{env.VERSION}} 28 | - name: pack Horarium.Mongo 29 | run: | 30 | cd src 31 | dotnet pack Horarium.Mongo/Horarium.Mongo.csproj -c Release /p:PackageVersion=${{env.VERSION}} 32 | - name: pack Horarium.AspNetCore 33 | run: | 34 | cd src 35 | dotnet pack Horarium.AspNetCore/Horarium.AspNetCore.csproj -c Release /p:PackageVersion=${{env.VERSION}} 36 | - name: publish 37 | run: | 38 | cd src 39 | dotnet nuget push **/Horarium.*.nupkg -k ${{secrets.NUGET_APIKEY}} -s https://www.nuget.org -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc 262 | 263 | \.sonarqube/ 264 | opencover.xml 265 | -------------------------------------------------------------------------------- /DistributedScheme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tinkoff/Horarium/718d7d4d990b88387876028aa6d850a042ebb481/DistributedScheme.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Horarium 2 | 3 | [![Build](https://github.com/Tinkoff/Horarium/actions/workflows/build.yml/badge.svg)](https://github.com/Tinkoff/Horarium/actions/workflows/build.yml) 4 | [![Nuget](https://img.shields.io/nuget/v/Horarium.svg)](https://www.nuget.org/packages/Horarium) 5 | 6 | Horarium is an open source job scheduling .NET library with an easy to use API, that can be integrated within applications of any scale - from the smallest stand-alone application to the largest e-commerce system. 7 | 8 | Horarium is fully based on an asynchronous work model, it allows you to run hundreds of parallel jobs within a single application instance. It supports jobs execution in distributed systems and uses MongoDB as a synchronization backend. 9 | 10 | Horarium supports .NET Core/netstandard 2.0 and .NET Framework 4.6.2 and later. 11 | 12 | Support Databases 13 | 14 | | Database | Support | 15 | | ---------- | ----------------------------------------------------------------------- | 16 | | MongoDB | Yes | 17 | | In Memory | Yes | 18 | | PostgreSQL | Not yet [#6](https://github.com/TinkoffCreditSystems/Horarium/issues/6) | 19 | 20 | ## Getting started 21 | 22 | Add nuget-package Horarium 23 | 24 | ```bash 25 | dotnet add package Horarium 26 | dotnet add package Horarium.Mongo 27 | ``` 28 | 29 | Add job that implements interface ```IJob``` 30 | 31 | ```csharp 32 | public class TestJob : IJob 33 | { 34 | public async Task Execute(int param) 35 | { 36 | Console.WriteLine(param); 37 | await Task.Run(() => { }); 38 | } 39 | } 40 | ``` 41 | 42 | Create ```HorariumServer``` and schedule ```TestJob``` 43 | 44 | ```csharp 45 | var horarium = new HorariumServer(new InMemoryRepository()); 46 | horarium.Start(); 47 | await horarium.Create(666) 48 | .Schedule(); 49 | ``` 50 | 51 | ## Add to ```Asp.Net core``` application 52 | 53 | Add nuget-package Horarium.AspNetCore 54 | 55 | ```bash 56 | dotnet add package Horarium.AspNetCore 57 | ``` 58 | 59 | Add ```Horarium Server```. This regiters Horarium as a [hosted service](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services), so .Net core runtime automatically starts and gracefully stops Horarium. 60 | 61 | ```csharp 62 | public void ConfigureServices(IServiceCollection services) 63 | { 64 | //... 65 | services.AddHorariumServer(MongoRepositoryFactory.Create("mongodb://localhost:27017/horarium")); 66 | //... 67 | } 68 | ``` 69 | 70 | Inject interface ```IHorarium``` into Controller 71 | 72 | ```csharp 73 | 74 | private readonly IHorarium _horarium; 75 | 76 | public HomeController(IHorarium horarium) 77 | { 78 | _horarium = horarium; 79 | } 80 | 81 | [Route("api")] 82 | public class HomeController : Controller 83 | { 84 | [HttpPost] 85 | public async Task Run(int count) 86 | { 87 | await _horarium.Create(count) 88 | .Schedule(); 89 | } 90 | } 91 | ``` 92 | 93 | ## Create Recurrent Job 94 | 95 | Add job that implements interface ```IJobRecurrent``` 96 | 97 | ```csharp 98 | public class TestRecurrentJob : IJobRecurrent 99 | { 100 | public Task Execute() 101 | { 102 | Console.WriteLine("Run -" + DateTime.Now); 103 | return Task.CompletedTask; 104 | } 105 | } 106 | ``` 107 | 108 | Schedule ```TestRecurrentJob``` to run every 15 seconds 109 | 110 | ```csharp 111 | await horarium.CreateRecurrent(Cron.SecondInterval(15)) 112 | .Schedule(); 113 | ``` 114 | 115 | ## Create sequence of jobs 116 | 117 | Sometimes you need to create sequence of jobs, where every next job would run if and only if previous job succeeds. If any job of the sequence fails next jobs won't run 118 | 119 | ```csharp 120 | await horarium 121 | .Create(1) // 1-st job 122 | .Next(2) // 2-nd job 123 | .Next(3) // 3-rd job 124 | .Schedule(); 125 | ``` 126 | 127 | ## Distributed Horarium 128 | 129 | ![Distributed Scheme](DistributedScheme.png) 130 | 131 | Horarium has two types of workers: server and client. Server can run jobs and schedule new jobs, while client can only schedule new jobs. 132 | 133 | Horarium guarantees that a job would run **exactly once** 134 | 135 | ## Things to watch out for 136 | 137 | Every Horarium instance consults MongoDB about new jobs to run every 100ms (default), thus creating some load on the DB server. This interval can be changed in ```HorariumSettings``` 138 | 139 | If you want to decrease load, you can use job throttling that will automatically increase interval if there are no jobs available after certain attempts. To enable this feature, pass `JobThrottleSettings` to `HorariumSettings` with property `UseJobThrottle` set to `true`. 140 | 141 | ```csharp 142 | var settings = new HorariumSettings 143 | { 144 | JobThrottleSettings = new JobThrottleSettings 145 | { 146 | UseJobThrottle = true 147 | } 148 | }; 149 | ``` 150 | 151 | For more information about configuration, see `JobThrottleSettings` 152 | 153 | ## Using Horarium with SimpleInjector 154 | 155 | To use Horarium with SimpleInjector one should implement its own `IJobFactory`, using `Container` from `SimpleInjector`. For example: 156 | ```csharp 157 | public class SimpleInjectorJobScopeFactory : IJobScopeFactory 158 | { 159 | private readonly Container _container; 160 | 161 | public SimpleInjectorJobScopeFactory(Container container) 162 | { 163 | _container = container; 164 | } 165 | 166 | public IJobScope Create() 167 | { 168 | var scope = AsyncScopedLifestyle.BeginScope(_container); 169 | return new SimpleInjectorJobScope(scope); 170 | } 171 | } 172 | 173 | public class SimpleInjectorJobScope : IJobScope 174 | { 175 | private readonly Scope _scope; 176 | 177 | public SimpleInjectorJobScope(Scope scope) 178 | { 179 | _scope = scope; 180 | } 181 | 182 | public object CreateJob(Type type) 183 | { 184 | return _scope.GetInstance(type); 185 | } 186 | 187 | public void Dispose() 188 | { 189 | _scope.Dispose(); 190 | } 191 | } 192 | ``` 193 | 194 | Then add `HorariumServer` (or `HorariumClient`): 195 | 196 | ```csharp 197 | container.RegisterSingleton(() => 198 | { 199 | var settings = new HorariumSettings 200 | { 201 | JobScopeFactory = new SimpleInjectorJobScopeFactory(container), 202 | Logger = new YourHorariumLogger() 203 | }; 204 | 205 | return new HorariumServer(jobRepository, settings); 206 | }); 207 | ``` 208 | 209 | In case of `HorariumServer`, don't forget to start it in your entypoint: 210 | 211 | ```csharp 212 | ((HorariumServer) container.GetInstance()).Start(); 213 | ``` 214 | 215 | ## Failed repeat strategy for jobs 216 | 217 | When a job fails, Horarium can handle this exception with the same strategy. 218 | By default, the job repeats 10 times with delays of 10 minutes, 20 minutes, 30 minutes and etc. 219 | You can override this strategy using `IFailedRepeatStrategy` interface. 220 | 221 | Example of default `DefaultRepeatStrategy` implementation: 222 | 223 | ```csharp 224 | public class DefaultRepeatStrategy :IFailedRepeatStrategy 225 | { 226 | public TimeSpan GetNextStartInterval(int countStarted) 227 | { 228 | const int increaseRepeat = 10; 229 | return TimeSpan.FromMinutes(increaseRepeat * countStarted); 230 | } 231 | } 232 | ``` 233 | 234 | This class is called every time when a job fails, and it has to return `TimeSpan` of the next scheduled job run. 235 | To override default behavior globally, change settings in ```HorariumSettings``` 236 | 237 | ```csharp 238 | new HorariumSettings 239 | { 240 | FailedRepeatStrategy = new CustomFailedRepeatStrategy(), 241 | MaxRepeatCount = 7 242 | }); 243 | ``` 244 | 245 | To override the default behavior for a particular job: 246 | 247 | ```csharp 248 | await horarium.Create(666) 249 | .MaxRepeatCount(5) 250 | .AddRepeatStrategy() 251 | .Schedule(); 252 | ``` 253 | 254 | If you want to disable all repeats, just set `MaxRepeatCount` to 1 255 | 256 | ```csharp 257 | new HorariumSettings 258 | { 259 | MaxRepeatCount = 1 260 | }); 261 | ``` 262 | 263 | -------------------------------------------------------------------------------- /src/Horarium.AspNetCore/Horarium.AspNetCore.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | Horarium.AspNetCore 6 | 1.0.0$(VersionSuffix) 7 | Horarium.AspNetCore 8 | bobreshovr 9 | Horarium.AspNetCore is the .Net library to support Asp .Net Core in Horarium 10 | https://github.com/TinkoffCreditSystems/Horarium 11 | https://github.com/TinkoffCreditSystems/Horarium 12 | Apache-2.0 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/Horarium.AspNetCore/HorariumLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Horarium.Interfaces; 3 | using Microsoft.Extensions.Logging; 4 | 5 | namespace Horarium.AspNetCore 6 | { 7 | public class HorariumLogger : IHorariumLogger 8 | { 9 | private readonly ILogger _logger; 10 | 11 | public HorariumLogger(ILogger logger) 12 | { 13 | _logger = logger; 14 | } 15 | 16 | public void Debug(string msg) 17 | { 18 | _logger.LogDebug(msg); 19 | } 20 | 21 | public void Debug(Exception ex) 22 | { 23 | _logger.LogDebug(ex, ex.Message); 24 | } 25 | 26 | public void Error(Exception ex) 27 | { 28 | _logger.LogError(ex, ex.Message); 29 | } 30 | 31 | public void Error(string message, Exception ex) 32 | { 33 | _logger.LogError(ex, message); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Horarium.AspNetCore/HorariumServerHostedService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Horarium.Interfaces; 4 | using Microsoft.Extensions.Hosting; 5 | 6 | namespace Horarium.AspNetCore 7 | { 8 | public class HorariumServerHostedService : IHostedService 9 | { 10 | private readonly HorariumServer _horariumServer; 11 | 12 | public HorariumServerHostedService(IHorarium horarium) 13 | { 14 | _horariumServer = (HorariumServer) horarium; 15 | } 16 | 17 | public Task StartAsync(CancellationToken cancellationToken) 18 | { 19 | _horariumServer.Start(); 20 | 21 | return Task.CompletedTask; 22 | } 23 | 24 | public Task StopAsync(CancellationToken cancellationToken) 25 | { 26 | return _horariumServer.Stop(cancellationToken); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/Horarium.AspNetCore/JobScope.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Horarium.Interfaces; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace Horarium.AspNetCore 6 | { 7 | public class JobScope : IJobScope 8 | { 9 | private readonly IServiceScope _serviceScope; 10 | 11 | public JobScope(IServiceScope serviceScope) 12 | { 13 | _serviceScope = serviceScope; 14 | } 15 | 16 | public object CreateJob(Type type) 17 | { 18 | return _serviceScope.ServiceProvider.GetService(type); 19 | } 20 | 21 | public void Dispose() 22 | { 23 | _serviceScope.Dispose(); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/Horarium.AspNetCore/JobScopeFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Horarium.Interfaces; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace Horarium.AspNetCore 6 | { 7 | public class JobScopeFactory : IJobScopeFactory 8 | { 9 | private readonly IServiceProvider _serviceProvider; 10 | 11 | public JobScopeFactory(IServiceProvider serviceProvider) 12 | { 13 | _serviceProvider = serviceProvider; 14 | } 15 | 16 | public IJobScope Create() 17 | { 18 | return new JobScope(_serviceProvider.CreateScope()); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/Horarium.AspNetCore/RegistrationHorariumExtension.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Horarium.Interfaces; 3 | using Horarium.Repository; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace Horarium.AspNetCore 8 | { 9 | public static class RegistrationHorariumExtension 10 | { 11 | public static IServiceCollection AddHorariumServer(this IServiceCollection service, 12 | IJobRepository jobRepository) 13 | { 14 | return service.AddHorariumServer(jobRepository, serviceProvider => new HorariumSettings()); 15 | } 16 | 17 | public static IServiceCollection AddHorariumServer(this IServiceCollection service, 18 | IJobRepository jobRepository, 19 | Func func) 20 | { 21 | service.AddSingleton(serviceProvider => 22 | { 23 | var settings = func(serviceProvider); 24 | 25 | PrepareSettings(settings, serviceProvider); 26 | 27 | return new HorariumServer(jobRepository, settings); 28 | }); 29 | 30 | service.AddHostedService(); 31 | 32 | return service; 33 | } 34 | 35 | public static IServiceCollection AddHorariumClient(this IServiceCollection service, 36 | IJobRepository jobRepository) 37 | { 38 | return service.AddHorariumClient(jobRepository, serviceProvider => new HorariumSettings()); 39 | } 40 | 41 | public static IServiceCollection AddHorariumClient(this IServiceCollection service, 42 | IJobRepository jobRepository, 43 | Func func) 44 | { 45 | service.AddSingleton(serviceProvider => 46 | { 47 | var settings = func(serviceProvider); 48 | 49 | PrepareSettings(settings, serviceProvider); 50 | 51 | return new HorariumClient(jobRepository, settings); 52 | }); 53 | 54 | return service; 55 | } 56 | 57 | private static void PrepareSettings(HorariumSettings settings, IServiceProvider serviceProvider) 58 | { 59 | if (settings.JobScopeFactory is DefaultJobScopeFactory) 60 | { 61 | settings.JobScopeFactory = new JobScopeFactory(serviceProvider); 62 | } 63 | 64 | if (settings.Logger is EmptyLogger) 65 | { 66 | settings.Logger = new HorariumLogger(serviceProvider.GetService>()); 67 | } 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /src/Horarium.InMemory/Horarium.InMemory.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Horarium.InMemory/InMemoryRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Threading.Tasks; 5 | using Horarium.Builders; 6 | using Horarium.Repository; 7 | 8 | namespace Horarium.InMemory 9 | { 10 | public class InMemoryRepository : IJobRepository 11 | { 12 | private readonly OperationsProcessor _processor = new OperationsProcessor(); 13 | 14 | private readonly JobsStorage _storage = new JobsStorage(); 15 | 16 | private readonly ConcurrentDictionary _settingsStorage = 17 | new ConcurrentDictionary(); 18 | 19 | public Task GetReadyJob(string machineName, TimeSpan obsoleteTime) 20 | { 21 | JobDb Query() 22 | { 23 | var job = _storage.FindReadyJob(obsoleteTime); 24 | if (job == null) return null; 25 | 26 | _storage.Remove(job.JobId); 27 | 28 | job.Status = JobStatus.Executing; 29 | job.ExecutedMachine = machineName; 30 | job.StartedExecuting = DateTime.UtcNow; 31 | job.CountStarted++; 32 | 33 | _storage.Add(job); 34 | 35 | return job; 36 | } 37 | 38 | return _processor.Execute(Query); 39 | } 40 | 41 | public Task AddJob(JobDb job) 42 | { 43 | return _processor.Execute(() => _storage.Add(job.Copy())); 44 | } 45 | 46 | public Task FailedJob(string jobId, Exception error) 47 | { 48 | return _processor.Execute(() => 49 | { 50 | var job = _storage.GetById(jobId); 51 | if (job == null) return; 52 | 53 | _storage.Remove(job); 54 | 55 | job.Status = JobStatus.Failed; 56 | job.Error = error.Message + ' ' + error.StackTrace; 57 | 58 | _storage.Add(job); 59 | }); 60 | } 61 | 62 | public Task RemoveJob(string jobId) 63 | { 64 | return _processor.Execute(() => _storage.Remove(jobId)); 65 | } 66 | 67 | public Task RepeatJob(string jobId, DateTime startAt, Exception error) 68 | { 69 | return _processor.Execute(() => 70 | { 71 | var job = _storage.GetById(jobId); 72 | if (job == null) return; 73 | 74 | _storage.Remove(job); 75 | 76 | job.Status = JobStatus.RepeatJob; 77 | job.StartAt = startAt; 78 | job.Error = error.Message + ' ' + error.StackTrace; 79 | 80 | _storage.Add(job); 81 | }); 82 | } 83 | 84 | public Task AddRecurrentJob(JobDb job) 85 | { 86 | return _processor.Execute(() => 87 | { 88 | var foundJob = _storage.FindRecurrentJobToUpdate(job.JobKey) ?? job.Copy(); 89 | 90 | _storage.Remove(foundJob); 91 | 92 | foundJob.Cron = job.Cron; 93 | foundJob.StartAt = job.StartAt; 94 | 95 | _storage.Add(foundJob); 96 | }); 97 | } 98 | 99 | public Task AddRecurrentJobSettings(RecurrentJobSettings settings) 100 | { 101 | _settingsStorage.AddOrUpdate(settings.JobKey, settings, (_, __) => settings); 102 | 103 | return Task.CompletedTask; 104 | } 105 | 106 | public Task> GetJobStatistic() 107 | { 108 | return Task.FromResult(_storage.GetStatistics()); 109 | } 110 | 111 | public Task RescheduleRecurrentJob(string jobId, DateTime startAt, Exception error) 112 | { 113 | return _processor.Execute(() => 114 | { 115 | var completedJob = _storage.GetById(jobId); 116 | if (completedJob == null) return; 117 | 118 | _storage.Remove(completedJob); 119 | 120 | if (error == null) 121 | { 122 | completedJob.Status = JobStatus.Ready; 123 | completedJob.StartAt = startAt; 124 | 125 | _storage.Add(completedJob); 126 | 127 | return; 128 | } 129 | 130 | completedJob.Status = JobStatus.Failed; 131 | completedJob.Error = error.Message + ' ' + error.StackTrace; 132 | 133 | _storage.Add(completedJob); 134 | 135 | var newJob = completedJob.Copy(); 136 | 137 | newJob.JobId = JobBuilderHelpers.GenerateNewJobId(); 138 | newJob.Status = JobStatus.Ready; 139 | newJob.StartAt = startAt; 140 | 141 | _storage.Add(newJob); 142 | }); 143 | } 144 | } 145 | } -------------------------------------------------------------------------------- /src/Horarium.InMemory/Indexes/Comparers/JobIdComparer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Horarium.Repository; 3 | 4 | namespace Horarium.InMemory.Indexes.Comparers 5 | { 6 | internal class JobIdComparer : IComparer 7 | { 8 | public int Compare(JobDb x, JobDb y) 9 | { 10 | return StaticJobIdComparer.Compare(x, y); 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /src/Horarium.InMemory/Indexes/Comparers/JobKeyComparer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Horarium.Repository; 4 | 5 | namespace Horarium.InMemory.Indexes.Comparers 6 | { 7 | internal class JobKeyComparer : IComparer 8 | { 9 | public int Compare(JobDb x, JobDb y) 10 | { 11 | var result = string.Compare(x.JobKey, y.JobKey, StringComparison.Ordinal); 12 | 13 | return result == 0 ? StaticJobIdComparer.Compare(x, y) : result; 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/Horarium.InMemory/Indexes/Comparers/StartAtComparer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Horarium.Repository; 3 | 4 | namespace Horarium.InMemory.Indexes.Comparers 5 | { 6 | internal class StartAtComparer : IComparer 7 | { 8 | public int Compare(JobDb x, JobDb y) 9 | { 10 | if (x.StartAt < y.StartAt) return -1; 11 | if (x.StartAt > y.StartAt) return 1; 12 | 13 | return StaticJobIdComparer.Compare(x, y); 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/Horarium.InMemory/Indexes/Comparers/StartedExecutingComparer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Horarium.Repository; 3 | 4 | namespace Horarium.InMemory.Indexes.Comparers 5 | { 6 | internal class StartedExecutingComparer : IComparer 7 | { 8 | public int Compare(JobDb x, JobDb y) 9 | { 10 | if (x.StartedExecuting < y.StartedExecuting) return -1; 11 | if (x.StartedExecuting > y.StartedExecuting) return 1; 12 | 13 | return StaticJobIdComparer.Compare(x, y); 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/Horarium.InMemory/Indexes/Comparers/StaticJobIdComparer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Horarium.Repository; 3 | 4 | namespace Horarium.InMemory.Indexes.Comparers 5 | { 6 | internal static class StaticJobIdComparer 7 | { 8 | public static int Compare(JobDb x, JobDb y) 9 | { 10 | return string.Compare(x.JobId, y.JobId, StringComparison.Ordinal); 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /src/Horarium.InMemory/Indexes/ExecutingJobIndex.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Horarium.InMemory.Indexes.Comparers; 4 | using Horarium.Repository; 5 | 6 | namespace Horarium.InMemory.Indexes 7 | { 8 | internal class ExecutingJobIndex : IAddRemoveIndex 9 | { 10 | private readonly SortedSet _startedExecutingIndex = new SortedSet(new StartedExecutingComparer()); 11 | 12 | private readonly JobKeyIndex _jobKeyIndex = new JobKeyIndex(); 13 | 14 | public void Add(JobDb job) 15 | { 16 | if (job.Status != JobStatus.Executing) return; 17 | 18 | _startedExecutingIndex.Add(job); 19 | _jobKeyIndex.Add(job); 20 | } 21 | 22 | public void Remove(JobDb job) 23 | { 24 | _startedExecutingIndex.Remove(job); 25 | _jobKeyIndex.Remove(job); 26 | } 27 | 28 | public int Count() 29 | { 30 | return _startedExecutingIndex.Count; 31 | } 32 | 33 | public JobDb GetJobKeyEqual(string jobKey) 34 | { 35 | return _jobKeyIndex.Get(jobKey); 36 | } 37 | 38 | public JobDb GetStartedExecutingLessThan(DateTime startedExecuting) 39 | { 40 | if (_startedExecutingIndex.Count != 0 && _startedExecutingIndex.Min.StartAt < startedExecuting) 41 | return _startedExecutingIndex.Min; 42 | 43 | return null; 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/Horarium.InMemory/Indexes/FailedJobIndex.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Horarium.Repository; 3 | 4 | namespace Horarium.InMemory.Indexes 5 | { 6 | internal class FailedJobIndex : IAddRemoveIndex 7 | { 8 | private readonly Dictionary _index = new Dictionary(); 9 | 10 | public void Add(JobDb job) 11 | { 12 | if (job.Status != JobStatus.Failed) return; 13 | 14 | _index.Add(job.JobId, job); 15 | } 16 | 17 | public void Remove(JobDb job) 18 | { 19 | _index.Remove(job.JobId); 20 | } 21 | 22 | public int Count() 23 | { 24 | return _index.Count; 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/Horarium.InMemory/Indexes/IAddRemoveIndex.cs: -------------------------------------------------------------------------------- 1 | using Horarium.Repository; 2 | 3 | namespace Horarium.InMemory.Indexes 4 | { 5 | internal interface IAddRemoveIndex 6 | { 7 | void Add(JobDb job); 8 | 9 | void Remove(JobDb job); 10 | 11 | int Count(); 12 | } 13 | } -------------------------------------------------------------------------------- /src/Horarium.InMemory/Indexes/JobKeyIndex.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Horarium.InMemory.Indexes.Comparers; 4 | using Horarium.Repository; 5 | 6 | namespace Horarium.InMemory.Indexes 7 | { 8 | internal class JobKeyIndex : IAddRemoveIndex 9 | { 10 | private readonly Dictionary> _index = new Dictionary>(); 11 | 12 | public void Add(JobDb job) 13 | { 14 | if (string.IsNullOrEmpty(job.JobKey)) return; 15 | 16 | if (!_index.TryGetValue(job.JobKey, out var set)) 17 | _index[job.JobKey] = new SortedSet(new JobIdComparer()) {job}; 18 | else 19 | set.Add(job); 20 | } 21 | 22 | public void Remove(JobDb job) 23 | { 24 | if (string.IsNullOrEmpty(job.JobKey)) return; 25 | 26 | if (!_index.TryGetValue(job.JobKey, out var set)) return; 27 | 28 | set.Remove(job); 29 | } 30 | 31 | public int Count() 32 | { 33 | throw new NotImplementedException(); 34 | } 35 | 36 | public JobDb Get(string jobKey) 37 | { 38 | if (!_index.TryGetValue(jobKey, out var set)) return null; 39 | 40 | if (set.Count != 0) return set.Min; 41 | 42 | return null; 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /src/Horarium.InMemory/Indexes/ReadyJobIndex.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Horarium.InMemory.Indexes.Comparers; 4 | using Horarium.Repository; 5 | 6 | namespace Horarium.InMemory.Indexes 7 | { 8 | internal class ReadyJobIndex : IAddRemoveIndex 9 | { 10 | private readonly SortedSet _startAtIndex = new SortedSet(new StartAtComparer()); 11 | 12 | private readonly JobKeyIndex _jobKeyIndex = new JobKeyIndex(); 13 | 14 | public void Add(JobDb job) 15 | { 16 | if (job.Status != JobStatus.Ready) return; 17 | 18 | _startAtIndex.Add(job); 19 | _jobKeyIndex.Add(job); 20 | } 21 | 22 | public void Remove(JobDb job) 23 | { 24 | _startAtIndex.Remove(job); 25 | _jobKeyIndex.Remove(job); 26 | } 27 | 28 | public int Count() 29 | { 30 | return _startAtIndex.Count; 31 | } 32 | 33 | public JobDb GetStartAtLessThan(DateTime startAt) 34 | { 35 | if (_startAtIndex.Count != 0 && _startAtIndex.Min.StartAt < startAt) 36 | return _startAtIndex.Min; 37 | 38 | return null; 39 | } 40 | 41 | public JobDb GetJobKeyEqual(string jobKey) 42 | { 43 | return _jobKeyIndex.Get(jobKey); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/Horarium.InMemory/Indexes/RepeatJobIndex.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Horarium.InMemory.Indexes.Comparers; 4 | using Horarium.Repository; 5 | 6 | namespace Horarium.InMemory.Indexes 7 | { 8 | internal class RepeatJobIndex : IAddRemoveIndex 9 | { 10 | private readonly SortedSet _startAtIndex = new SortedSet(new StartAtComparer()); 11 | 12 | public void Add(JobDb job) 13 | { 14 | if (job.Status != JobStatus.RepeatJob) return; 15 | 16 | _startAtIndex.Add(job); 17 | } 18 | 19 | public void Remove(JobDb job) 20 | { 21 | _startAtIndex.Remove(job); 22 | } 23 | 24 | public int Count() 25 | { 26 | return _startAtIndex.Count; 27 | } 28 | 29 | public JobDb GetStartAtLessThan(DateTime startAt) 30 | { 31 | if (_startAtIndex.Count != 0 && _startAtIndex.Min.StartAt < startAt) 32 | return _startAtIndex.Min; 33 | 34 | return null; 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /src/Horarium.InMemory/JobDbExtension.cs: -------------------------------------------------------------------------------- 1 | using Horarium.Repository; 2 | 3 | namespace Horarium.InMemory 4 | { 5 | internal static class JobDbExtension 6 | { 7 | public static JobDb Copy(this JobDb source) 8 | { 9 | return new JobDb 10 | { 11 | JobKey = source.JobKey, 12 | JobId = source.JobId, 13 | Status = source.Status, 14 | JobType = source.JobType, 15 | JobParamType = source.JobParamType, 16 | JobParam = source.JobParam, 17 | CountStarted = source.CountStarted, 18 | StartedExecuting = source.StartedExecuting, 19 | ExecutedMachine = source.ExecutedMachine, 20 | StartAt = source.StartAt, 21 | NextJob = source.NextJob?.Copy(), 22 | Cron = source.Cron, 23 | Delay = source.Delay, 24 | ObsoleteInterval = source.ObsoleteInterval, 25 | RepeatStrategy = source.RepeatStrategy, 26 | MaxRepeatCount = source.MaxRepeatCount, 27 | FallbackJob = source.FallbackJob?.Copy(), 28 | FallbackStrategyType = source.FallbackStrategyType 29 | }; 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/Horarium.InMemory/JobsStorage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Horarium.InMemory.Indexes; 4 | using Horarium.Repository; 5 | 6 | namespace Horarium.InMemory 7 | { 8 | internal class JobsStorage 9 | { 10 | private readonly Dictionary _jobs = new Dictionary(); 11 | 12 | private readonly ReadyJobIndex _readyJobIndex = new ReadyJobIndex(); 13 | private readonly ExecutingJobIndex _executingJobIndex = new ExecutingJobIndex(); 14 | private readonly RepeatJobIndex _repeatJobIndex = new RepeatJobIndex(); 15 | private readonly FailedJobIndex _failedJobIndex = new FailedJobIndex(); 16 | 17 | private readonly List _indexes; 18 | 19 | public JobsStorage() 20 | { 21 | _indexes = new List 22 | { 23 | _readyJobIndex, 24 | _executingJobIndex, 25 | _repeatJobIndex, 26 | _failedJobIndex 27 | }; 28 | } 29 | 30 | public void Add(JobDb job) 31 | { 32 | _jobs.Add(job.JobId, job); 33 | 34 | _indexes.ForEach(x => x.Add(job)); 35 | } 36 | 37 | public void Remove(string jobId) 38 | { 39 | if (!_jobs.TryGetValue(jobId, out var job)) return; 40 | 41 | Remove(job); 42 | } 43 | 44 | public void Remove(JobDb job) 45 | { 46 | _jobs.Remove(job.JobId); 47 | 48 | _indexes.ForEach(x => x.Remove(job)); 49 | } 50 | 51 | public Dictionary GetStatistics() 52 | { 53 | return new Dictionary 54 | { 55 | {JobStatus.Ready, _readyJobIndex.Count()}, 56 | {JobStatus.Executing, _executingJobIndex.Count()}, 57 | {JobStatus.RepeatJob, _repeatJobIndex.Count()}, 58 | {JobStatus.Failed, _failedJobIndex.Count()} 59 | }; 60 | } 61 | 62 | public JobDb GetById(string jobId) 63 | { 64 | if (!_jobs.TryGetValue(jobId, out var job)) return null; 65 | 66 | return job; 67 | } 68 | 69 | public JobDb FindRecurrentJobToUpdate(string jobKey) 70 | { 71 | return _readyJobIndex.GetJobKeyEqual(jobKey) ?? _executingJobIndex.GetJobKeyEqual(jobKey); 72 | } 73 | 74 | public JobDb FindReadyJob(TimeSpan obsoleteTime) 75 | { 76 | var now = DateTime.UtcNow; 77 | 78 | return _readyJobIndex.GetStartAtLessThan(now) ?? 79 | _repeatJobIndex.GetStartAtLessThan(now) ?? 80 | _executingJobIndex.GetStartedExecutingLessThan(now - obsoleteTime); 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /src/Horarium.InMemory/OperationsProcessor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Channels; 3 | using System.Threading.Tasks; 4 | using Horarium.Repository; 5 | 6 | namespace Horarium.InMemory 7 | { 8 | internal class OperationsProcessor 9 | { 10 | private readonly Channel _channel = Channel.CreateUnbounded(); 11 | 12 | public OperationsProcessor() 13 | { 14 | Task.Run(ProcessQueue); 15 | } 16 | 17 | private async Task ProcessQueue() 18 | { 19 | while (await _channel.Reader.WaitToReadAsync()) 20 | { 21 | while (_channel.Reader.TryRead(out var operation)) 22 | { 23 | operation.Execute(); 24 | } 25 | } 26 | } 27 | 28 | public Task Execute(Action command) 29 | { 30 | var wrapped = new CommandWrapper(command); 31 | _channel.Writer.TryWrite(wrapped); 32 | 33 | return wrapped.Task; 34 | } 35 | 36 | public Task Execute(Func query) 37 | { 38 | var wrapped = new QueryWrapper(query); 39 | _channel.Writer.TryWrite(wrapped); 40 | 41 | return wrapped.Task; 42 | } 43 | 44 | private abstract class BaseWrapper 45 | { 46 | protected readonly TaskCompletionSource CompletionSource = 47 | new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); 48 | 49 | public abstract void Execute(); 50 | 51 | public Task Task => CompletionSource.Task; 52 | } 53 | 54 | private class QueryWrapper : BaseWrapper 55 | { 56 | private readonly Func _query; 57 | 58 | public QueryWrapper(Func query) 59 | { 60 | _query = query; 61 | } 62 | 63 | public override void Execute() 64 | { 65 | CompletionSource.SetResult(_query()?.Copy()); 66 | } 67 | } 68 | 69 | private class CommandWrapper : BaseWrapper 70 | { 71 | private readonly Action _command; 72 | 73 | public CommandWrapper(Action command) 74 | { 75 | _command = command; 76 | } 77 | 78 | public override void Execute() 79 | { 80 | _command(); 81 | 82 | CompletionSource.SetResult(null); 83 | } 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /src/Horarium.IntegrationTest/FallbackJobTest.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Horarium.IntegrationTest.Jobs.Fallback; 3 | using Xunit; 4 | 5 | namespace Horarium.IntegrationTest 6 | { 7 | [Collection(IntegrationTestCollection)] 8 | public class FallbackJobTest : IntegrationTestBase 9 | { 10 | public FallbackJobTest() 11 | { 12 | FallbackNextJob.ExecutedCount = 0; 13 | FallbackMainJob.ExecutedCount = 0; 14 | FallbackJob.ExecutedCount = 0; 15 | } 16 | 17 | [Fact] 18 | public async Task FallbackJobAdded_FallbackJobExecuted() 19 | { 20 | var horarium = CreateHorariumServer(); 21 | 22 | var mainJobRepeatCount = 2; 23 | await horarium.Schedule(1, conf => 24 | conf.MaxRepeatCount(mainJobRepeatCount) 25 | .AddRepeatStrategy() 26 | .AddFallbackConfiguration(configure => 27 | configure 28 | .ScheduleFallbackJob( 29 | 2, 30 | builder => 31 | { 32 | builder 33 | .Next(3); 35 | }))); 36 | 37 | await Task.Delay(7000); 38 | 39 | horarium.Dispose(); 40 | 41 | Assert.Equal(mainJobRepeatCount, FallbackMainJob.ExecutedCount); 42 | Assert.Equal(1, FallbackJob.ExecutedCount); 43 | Assert.Equal(1, FallbackNextJob.ExecutedCount); 44 | } 45 | 46 | [Fact] 47 | public async Task FallbackJobGoNextStrategy_NextJobExecuted() 48 | { 49 | var horarium = CreateHorariumServer(); 50 | 51 | var mainJobRepeatCount = 2; 52 | await horarium.Schedule(1, conf => 53 | conf.MaxRepeatCount(mainJobRepeatCount) 54 | .AddRepeatStrategy() 55 | .AddFallbackConfiguration( 56 | configure => configure.GoToNextJob()) 57 | .Next(2) 58 | ); 59 | 60 | await Task.Delay(7000); 61 | 62 | horarium.Dispose(); 63 | 64 | Assert.Equal(mainJobRepeatCount, FallbackMainJob.ExecutedCount); 65 | Assert.Equal(1, FallbackNextJob.ExecutedCount); 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /src/Horarium.IntegrationTest/Horarium.IntegrationTest.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp2.2 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/Horarium.IntegrationTest/IntegrationTestBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Horarium.InMemory; 3 | using Horarium.Mongo; 4 | using Horarium.Repository; 5 | 6 | namespace Horarium.IntegrationTest 7 | { 8 | public class IntegrationTestBase 9 | { 10 | protected const string IntegrationTestCollection = "IntegrationTestCollection"; 11 | private string DatabaseNameMongo => "IntegrationTestHorarium" + Guid.NewGuid(); 12 | 13 | private string ConnectionMongo => $"mongodb://{Environment.GetEnvironmentVariable("MONGO_ADDRESS") ?? "localhost"}:27017/{DatabaseNameMongo}"; 14 | 15 | protected HorariumServer CreateHorariumServer() 16 | { 17 | var dataBase = Environment.GetEnvironmentVariable("DataBase"); 18 | 19 | IJobRepository jobRepository; 20 | 21 | switch (dataBase) 22 | { 23 | case "MongoDB": 24 | jobRepository = MongoRepositoryFactory.Create(ConnectionMongo); 25 | break; 26 | case "Memory": 27 | jobRepository = new InMemoryRepository(); 28 | break; 29 | default: 30 | throw new ArgumentOutOfRangeException(nameof(dataBase), dataBase, null); 31 | } 32 | 33 | var horarium = new HorariumServer(jobRepository); 34 | horarium.Start(); 35 | 36 | return horarium; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Horarium.IntegrationTest/Jobs/Fallback/FallbackJob.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Horarium.Interfaces; 3 | 4 | namespace Horarium.IntegrationTest.Jobs.Fallback 5 | { 6 | public class FallbackJob : IJob 7 | { 8 | public static int ExecutedCount { get; set; } 9 | 10 | public Task Execute(int param) 11 | { 12 | ExecutedCount++; 13 | 14 | return Task.CompletedTask; 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /src/Horarium.IntegrationTest/Jobs/Fallback/FallbackMainJob.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Horarium.Interfaces; 4 | 5 | namespace Horarium.IntegrationTest.Jobs.Fallback 6 | { 7 | public class FallbackMainJob : IJob 8 | { 9 | public static int ExecutedCount { get; set; } 10 | 11 | public Task Execute(int param) 12 | { 13 | ExecutedCount++; 14 | 15 | throw new Exception(); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/Horarium.IntegrationTest/Jobs/Fallback/FallbackNextJob.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Horarium.Interfaces; 3 | 4 | namespace Horarium.IntegrationTest.Jobs.Fallback 5 | { 6 | public class FallbackNextJob : IJob 7 | { 8 | public static int ExecutedCount { get; set; } 9 | 10 | public Task Execute(int param) 11 | { 12 | ExecutedCount++; 13 | 14 | return Task.CompletedTask; 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /src/Horarium.IntegrationTest/Jobs/Fallback/FallbackRepeatStrategy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Horarium.Interfaces; 3 | 4 | namespace Horarium.IntegrationTest.Jobs.Fallback 5 | { 6 | public class FallbackRepeatStrategy : IFailedRepeatStrategy 7 | { 8 | public TimeSpan GetNextStartInterval(int countStarted) 9 | { 10 | return TimeSpan.FromSeconds(5); 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /src/Horarium.IntegrationTest/Jobs/OneTimeJob.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Horarium.Interfaces; 3 | 4 | namespace Horarium.IntegrationTest.Jobs 5 | { 6 | public class OneTimeJob : IJob 7 | { 8 | public static bool Run; 9 | 10 | public async Task Execute(int param) 11 | { 12 | Run = true; 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/Horarium.IntegrationTest/Jobs/RecurrentJob.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Threading.Tasks; 4 | using Horarium.Interfaces; 5 | 6 | namespace Horarium.IntegrationTest.Jobs 7 | { 8 | public class RecurrentJob : IJobRecurrent 9 | { 10 | public static readonly ConcurrentQueue ExecutingTime = new ConcurrentQueue(); 11 | 12 | public Task Execute() 13 | { 14 | ExecutingTime.Enqueue(DateTime.Now); 15 | 16 | return Task.CompletedTask; 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /src/Horarium.IntegrationTest/Jobs/RecurrentJobForUpdate.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using System.Threading.Tasks; 3 | using Horarium.Interfaces; 4 | 5 | namespace Horarium.IntegrationTest.Jobs 6 | { 7 | public class RecurrentJobForUpdate : IJobRecurrent 8 | { 9 | public static readonly ConcurrentStack StackJobs = new ConcurrentStack(); 10 | 11 | public async Task Execute() 12 | { 13 | StackJobs.Push(this); 14 | await Task.Delay(1000000); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Horarium.IntegrationTest/Jobs/RepeatFailedJob.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Threading.Tasks; 4 | using Horarium.Interfaces; 5 | 6 | namespace Horarium.IntegrationTest.Jobs 7 | { 8 | public class RepeatFailedJob : IJob, IAllRepeatesIsFailed 9 | { 10 | public static readonly ConcurrentQueue ExecutingTime = new ConcurrentQueue(); 11 | 12 | public static bool RepeatIsOk; 13 | 14 | public Task Execute(string param) 15 | { 16 | ExecutingTime.Enqueue(DateTime.Now); 17 | 18 | throw new Exception(); 19 | } 20 | 21 | public Task FailedEvent(object param, Exception ex) 22 | { 23 | RepeatIsOk = true; 24 | return Task.CompletedTask; 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/Horarium.IntegrationTest/Jobs/SequenceJob.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using System.Threading.Tasks; 3 | using Horarium.Interfaces; 4 | 5 | namespace Horarium.IntegrationTest.Jobs 6 | { 7 | public class SequenceJob : IJob 8 | { 9 | public static readonly ConcurrentQueue QueueJobs = new ConcurrentQueue(); 10 | 11 | public Task Execute(int param) 12 | { 13 | QueueJobs.Enqueue(param); 14 | return Task.CompletedTask; 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /src/Horarium.IntegrationTest/Jobs/TestJob.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using System.Threading.Tasks; 3 | using Horarium.Interfaces; 4 | 5 | namespace Horarium.IntegrationTest.Jobs 6 | { 7 | public class TestJob : IJob 8 | { 9 | public static readonly ConcurrentStack StackJobs = new ConcurrentStack(); 10 | 11 | public async Task Execute(int param) 12 | { 13 | StackJobs.Push(param); 14 | await Task.Delay(30); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /src/Horarium.IntegrationTest/Jobs/TestJobParam.cs: -------------------------------------------------------------------------------- 1 | namespace Horarium.IntegrationTest.Jobs 2 | { 3 | public class TestJobParam 4 | { 5 | public int Counter { get; set; } 6 | } 7 | } -------------------------------------------------------------------------------- /src/Horarium.IntegrationTest/Jobs/TestObsoleteJob.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Threading.Tasks; 4 | using Horarium.Interfaces; 5 | 6 | namespace Horarium.IntegrationTest.Jobs 7 | { 8 | public class TestObsoleteJob : IJob 9 | { 10 | public static TimeSpan JobExecutionTimeSeconds = TimeSpan.FromSeconds(5); 11 | public static readonly ConcurrentStack JobsStack = new ConcurrentStack(); 12 | 13 | public Task Execute(int param) 14 | { 15 | JobsStack.Push(this); 16 | return Task.Delay(JobExecutionTimeSeconds); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /src/Horarium.IntegrationTest/OneTimeJobTest.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Horarium.IntegrationTest.Jobs; 3 | using Xunit; 4 | 5 | namespace Horarium.IntegrationTest 6 | { 7 | [Collection(IntegrationTestCollection)] 8 | public class OneTimeJobTest: IntegrationTestBase 9 | { 10 | [Fact] 11 | public async Task OneTimeJob_RunAfterAdded() 12 | { 13 | var horarium = CreateHorariumServer(); 14 | 15 | await horarium.Create(5).Schedule(); 16 | 17 | await Task.Delay(1000); 18 | 19 | horarium.Dispose(); 20 | 21 | Assert.True(OneTimeJob.Run); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/Horarium.IntegrationTest/RecurrentJobTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Horarium.IntegrationTest.Jobs; 6 | using Xunit; 7 | 8 | namespace Horarium.IntegrationTest 9 | { 10 | [Collection(IntegrationTestCollection)] 11 | public class RecurrentJobTest : IntegrationTestBase 12 | { 13 | [Fact] 14 | public async Task RecurrentJob_RunEverySeconds() 15 | { 16 | var horarium = CreateHorariumServer(); 17 | 18 | await horarium.CreateRecurrent(Cron.Secondly()).Schedule(); 19 | 20 | await Task.Delay(10000); 21 | 22 | horarium.Dispose(); 23 | 24 | var executingTimes = RecurrentJob.ExecutingTime.ToArray(); 25 | 26 | Assert.NotEmpty(executingTimes); 27 | 28 | var nextJobTime = executingTimes.First(); 29 | 30 | foreach (var time in executingTimes) 31 | { 32 | Assert.Equal(nextJobTime, time, TimeSpan.FromMilliseconds(999)); 33 | nextJobTime = time.AddSeconds(1); 34 | } 35 | } 36 | 37 | /// 38 | /// Тест проверяет, что при одновременной регистрации одного джоба разными шедулерами первый начнет выполняться, а второй нет, 39 | /// т.к. для рекуррентных джобов одновременно может выполняться только один экземпляр 40 | /// 41 | /// 42 | [Fact] 43 | public async Task Scheduler_SecondInstanceStart_MustUpdateRecurrentJobCronParameters() 44 | { 45 | var watch = Stopwatch.StartNew(); 46 | var scheduler = CreateHorariumServer(); 47 | 48 | while (true) 49 | { 50 | await scheduler.CreateRecurrent(Cron.SecondInterval(1)).Schedule(); 51 | 52 | if (watch.Elapsed > TimeSpan.FromSeconds(15)) 53 | { 54 | break; 55 | } 56 | } 57 | 58 | await Task.Delay(TimeSpan.FromSeconds(5)); 59 | 60 | scheduler.Dispose(); 61 | 62 | Assert.Single(RecurrentJobForUpdate.StackJobs); 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /src/Horarium.IntegrationTest/RepeatFailedJobTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Horarium.IntegrationTest.Jobs; 6 | using Horarium.Interfaces; 7 | using Xunit; 8 | 9 | namespace Horarium.IntegrationTest 10 | { 11 | [Collection(IntegrationTestCollection)] 12 | public class RepeatFailedJobTest : IntegrationTestBase 13 | { 14 | [Fact] 15 | public async Task RepeatFailedJob_UseRepeatStrategy() 16 | { 17 | var horarium = CreateHorariumServer(); 18 | 19 | await horarium.Create(string.Empty) 20 | .AddRepeatStrategy() 21 | .MaxRepeatCount(5) 22 | .Schedule(); 23 | 24 | var watch = Stopwatch.StartNew(); 25 | 26 | while (!RepeatFailedJob.RepeatIsOk) 27 | { 28 | await Task.Delay(100); 29 | 30 | if (watch.Elapsed >= TimeSpan.FromSeconds(10)) 31 | { 32 | throw new Exception("Time is over"); 33 | } 34 | } 35 | 36 | var executingTimes = RepeatFailedJob.ExecutingTime.ToArray(); 37 | 38 | Assert.Equal(5, executingTimes.Length); 39 | 40 | var nextJobTime = executingTimes.First(); 41 | 42 | foreach (var time in executingTimes) 43 | { 44 | Assert.Equal(nextJobTime, time, TimeSpan.FromMilliseconds(999)); 45 | nextJobTime = time.AddSeconds(1); 46 | } 47 | 48 | } 49 | } 50 | 51 | public class RepeatFailedJobTestStrategy: IFailedRepeatStrategy 52 | { 53 | public TimeSpan GetNextStartInterval(int countStarted) 54 | { 55 | return TimeSpan.FromSeconds(1); 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /src/Horarium.IntegrationTest/SequenceJobTest.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Horarium.IntegrationTest.Jobs; 3 | using Xunit; 4 | 5 | namespace Horarium.IntegrationTest 6 | { 7 | [Collection(IntegrationTestCollection)] 8 | public class SequenceJobTest : IntegrationTestBase 9 | { 10 | [Fact] 11 | public async Task SequenceJobsAdded_ExecutedSequence() 12 | { 13 | var horarium = CreateHorariumServer(); 14 | 15 | await horarium.Create(0) 16 | .Next(1) 17 | .Next(2) 18 | .Schedule(); 19 | 20 | await Task.Delay(1000); 21 | 22 | horarium.Dispose(); 23 | 24 | var queueJobs = SequenceJob.QueueJobs.ToArray(); 25 | 26 | Assert.NotEmpty(queueJobs); 27 | 28 | Assert.Equal(new [] {0,1,2}, queueJobs); 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /src/Horarium.IntegrationTest/TestParallelsWorkTwoManagers.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using Horarium.IntegrationTest.Jobs; 4 | using Xunit; 5 | 6 | namespace Horarium.IntegrationTest 7 | { 8 | [Collection(IntegrationTestCollection)] 9 | public class TestParallelsWorkTwoManagers : IntegrationTestBase 10 | { 11 | [Fact] 12 | public async Task TestParallels() 13 | { 14 | var firstScheduler = CreateHorariumServer(); 15 | var secondScheduler = CreateHorariumServer(); 16 | 17 | for (var i = 0; i < 1000; i++) 18 | { 19 | await firstScheduler.Create(i).Schedule(); 20 | await Task.Delay(10); 21 | } 22 | 23 | await Task.Delay(10000); 24 | 25 | firstScheduler.Dispose(); 26 | secondScheduler.Dispose(); 27 | 28 | Assert.NotEmpty(TestJob.StackJobs); 29 | 30 | Assert.False(TestJob.StackJobs.GroupBy(x => x).Any(g => g.Count() > 1), 31 | "Same job was executed multiple times"); 32 | } 33 | 34 | } 35 | } -------------------------------------------------------------------------------- /src/Horarium.Mongo/Horarium.Mongo.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | Horarium.Mongo 6 | 1.0.0$(VersionSuffix) 7 | Horarium.Mongo 8 | bobreshovr 9 | Horarium.Mongo is the .Net library to support MongoDB in Horarium 10 | https://github.com/TinkoffCreditSystems/Horarium 11 | https://github.com/TinkoffCreditSystems/Horarium 12 | Apache-2.0 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Horarium.Mongo/IMongoClientProvider.cs: -------------------------------------------------------------------------------- 1 | using MongoDB.Driver; 2 | 3 | namespace Horarium.Mongo 4 | { 5 | public interface IMongoClientProvider 6 | { 7 | IMongoCollection GetCollection(); 8 | } 9 | } -------------------------------------------------------------------------------- /src/Horarium.Mongo/JobMongoModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Horarium.Fallbacks; 3 | using Horarium.Repository; 4 | using MongoDB.Bson; 5 | using MongoDB.Bson.Serialization.Attributes; 6 | using MongoDB.Bson.Serialization.Options; 7 | 8 | namespace Horarium.Mongo 9 | { 10 | [MongoEntity("horarium.jobs")] 11 | public class JobMongoModel 12 | { 13 | public JobDb ToJobDb() 14 | { 15 | return new JobDb 16 | { 17 | JobKey = JobKey, 18 | JobId = JobId, 19 | Status = Status, 20 | JobType = JobType, 21 | JobParamType = JobParamType, 22 | JobParam = JobParam, 23 | CountStarted = CountStarted, 24 | StartedExecuting = StartedExecuting, 25 | ExecutedMachine = ExecutedMachine, 26 | StartAt = StartAt, 27 | NextJob = NextJob?.ToJobDb(), 28 | Cron = Cron, 29 | Delay = Delay, 30 | ObsoleteInterval = ObsoleteInterval, 31 | RepeatStrategy = RepeatStrategy, 32 | MaxRepeatCount = MaxRepeatCount, 33 | FallbackJob = FallbackJob?.ToJobDb(), 34 | FallbackStrategyType = FallbackStrategyType 35 | }; 36 | } 37 | 38 | [BsonId] 39 | [BsonRepresentation(BsonType.String)] 40 | public string JobId { get; set; } 41 | 42 | [BsonRepresentation(BsonType.String)] 43 | [BsonElement("JobKey")] 44 | public string JobKey { get; set; } 45 | 46 | [BsonRepresentation(BsonType.String)] 47 | [BsonElement("JobType")] 48 | public string JobType { get; set; } 49 | 50 | [BsonRepresentation(BsonType.String)] 51 | [BsonElement("JobParamType")] 52 | public string JobParamType { get; set; } 53 | 54 | [BsonRepresentation(BsonType.String)] 55 | [BsonElement("JobParam")] 56 | public string JobParam { get; set; } 57 | 58 | [BsonRepresentation(BsonType.Int32)] 59 | [BsonElement("Status")] 60 | public JobStatus Status { get; set; } 61 | 62 | [BsonRepresentation(BsonType.Int32)] 63 | [BsonElement("CountStarted")] 64 | public int CountStarted { get; set; } 65 | 66 | [BsonRepresentation(BsonType.String)] 67 | [BsonElement("ExecutedMachine")] 68 | public string ExecutedMachine { get; set; } 69 | 70 | [BsonDateTimeOptions(Kind = DateTimeKind.Utc, DateOnly = false, Representation = BsonType.DateTime)] 71 | [BsonElement("StartedExecuting")] 72 | public DateTime StartedExecuting { get; set; } 73 | 74 | [BsonDateTimeOptions(Kind = DateTimeKind.Utc, DateOnly = false, Representation = BsonType.DateTime)] 75 | [BsonElement("StartAt")] 76 | public DateTime StartAt { get; set; } 77 | 78 | [BsonElement("NextJob")] 79 | public JobMongoModel NextJob { get; set; } 80 | 81 | [BsonRepresentation(BsonType.String)] 82 | [BsonElement("Error")] 83 | public string Error { get; set; } 84 | 85 | [BsonRepresentation(BsonType.String)] 86 | [BsonElement("Cron")] 87 | public string Cron { get; set; } 88 | 89 | [BsonTimeSpanOptions(BsonType.String)] 90 | [BsonElement("Delay")] 91 | public TimeSpan? Delay { get; set; } 92 | 93 | [BsonTimeSpanOptions(BsonType.Int64, TimeSpanUnits.Milliseconds)] 94 | [BsonElement("ObsoleteInterval")] 95 | public TimeSpan ObsoleteInterval { get; set; } 96 | 97 | [BsonRepresentation(BsonType.String)] 98 | [BsonElement("RepeatStrategy")] 99 | public string RepeatStrategy { get; set; } 100 | 101 | [BsonRepresentation(BsonType.Int32)] 102 | [BsonElement("MaxRepeatCount")] 103 | public int MaxRepeatCount { get; set; } 104 | 105 | [BsonElement("FallbackJob")] 106 | public JobMongoModel FallbackJob { get; set; } 107 | 108 | [BsonRepresentation(BsonType.String)] 109 | [BsonElement("FallbackStrategyType")] 110 | public FallbackStrategyTypeEnum? FallbackStrategyType { get; set; } 111 | 112 | public static JobMongoModel CreateJobMongoModel(JobDb jobDb) 113 | { 114 | return new JobMongoModel 115 | { 116 | JobId = jobDb.JobId, 117 | JobKey = jobDb.JobKey, 118 | Status = jobDb.Status, 119 | CountStarted = jobDb.CountStarted, 120 | StartedExecuting = jobDb.StartedExecuting, 121 | ExecutedMachine = jobDb.ExecutedMachine, 122 | JobType = jobDb.JobType, 123 | JobParam = jobDb.JobParam, 124 | JobParamType = jobDb.JobParamType, 125 | StartAt = jobDb.StartAt, 126 | NextJob = jobDb.NextJob != null ? CreateJobMongoModel(jobDb.NextJob) : null, 127 | Cron = jobDb.Cron, 128 | Delay = jobDb.Delay, 129 | ObsoleteInterval = jobDb.ObsoleteInterval, 130 | RepeatStrategy = jobDb.RepeatStrategy, 131 | MaxRepeatCount = jobDb.MaxRepeatCount, 132 | FallbackStrategyType = jobDb.FallbackStrategyType, 133 | FallbackJob = jobDb.FallbackJob != null ? CreateJobMongoModel(jobDb.FallbackJob) : null, 134 | }; 135 | } 136 | } 137 | } -------------------------------------------------------------------------------- /src/Horarium.Mongo/MongoClientProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Reflection; 4 | using MongoDB.Driver; 5 | 6 | namespace Horarium.Mongo 7 | { 8 | public sealed class MongoClientProvider : IMongoClientProvider 9 | { 10 | private readonly ConcurrentDictionary _collectionNameCache = new ConcurrentDictionary(); 11 | 12 | private readonly MongoClient _mongoClient; 13 | private readonly string _databaseName; 14 | private bool _initialized; 15 | private object _lockObject = new object(); 16 | 17 | public MongoClientProvider(MongoUrl mongoUrl) 18 | { 19 | _databaseName = mongoUrl.DatabaseName; 20 | _mongoClient = new MongoClient(mongoUrl); 21 | } 22 | 23 | public MongoClientProvider(string mongoConnectionString): this (new MongoUrl(mongoConnectionString)) 24 | { 25 | } 26 | 27 | private string GetCollectionName(Type entityType) 28 | { 29 | var collectionAttr = entityType.GetTypeInfo().GetCustomAttribute(); 30 | 31 | if (collectionAttr == null) 32 | throw new InvalidOperationException($"Entity with type '{entityType.GetTypeInfo().FullName}' is not Mongo entity (use MongoEntityAttribute)"); 33 | 34 | return collectionAttr.CollectionName; 35 | } 36 | 37 | public IMongoCollection GetCollection() 38 | { 39 | EnsureInitialized(); 40 | 41 | var collectionName = _collectionNameCache.GetOrAdd(typeof(TEntity), GetCollectionName); 42 | return _mongoClient.GetDatabase(_databaseName).GetCollection(collectionName); 43 | } 44 | 45 | private void EnsureInitialized() 46 | { 47 | if (_initialized) 48 | return; 49 | 50 | lock (_lockObject) 51 | { 52 | if (_initialized) 53 | return; 54 | 55 | _initialized = true; 56 | CreateIndexes(); 57 | } 58 | } 59 | 60 | private void CreateIndexes() 61 | { 62 | var indexKeyBuilder = Builders.IndexKeys; 63 | 64 | var collection = GetCollection(); 65 | 66 | collection.Indexes.CreateMany(new[] 67 | { 68 | new CreateIndexModel( 69 | indexKeyBuilder 70 | .Ascending(x => x.Status) 71 | .Ascending(x=>x.StartAt) 72 | .Ascending(x=>x.StartedExecuting), 73 | new CreateIndexOptions 74 | { 75 | Background = true 76 | }), 77 | 78 | new CreateIndexModel( 79 | indexKeyBuilder 80 | .Ascending(x => x.Status) 81 | .Ascending(x => x.JobKey), 82 | new CreateIndexOptions 83 | { 84 | Background = true 85 | }), 86 | 87 | new CreateIndexModel( 88 | indexKeyBuilder 89 | .Ascending(x => x.JobKey), 90 | new CreateIndexOptions 91 | { 92 | Background = true 93 | }) 94 | }); 95 | } 96 | } 97 | } -------------------------------------------------------------------------------- /src/Horarium.Mongo/MongoEntityAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Horarium.Mongo 4 | { 5 | public class MongoEntityAttribute: Attribute 6 | { 7 | public MongoEntityAttribute(string collectionName) 8 | { 9 | CollectionName = collectionName; 10 | } 11 | 12 | public string CollectionName { get; } 13 | } 14 | } -------------------------------------------------------------------------------- /src/Horarium.Mongo/MongoRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Threading.Tasks; 6 | using Horarium.Builders; 7 | using Horarium.Repository; 8 | using MongoDB.Driver; 9 | 10 | namespace Horarium.Mongo 11 | { 12 | public class MongoRepository : IJobRepository 13 | { 14 | private readonly PropertyInfo[] _jobDbProperties; 15 | private readonly IMongoClientProvider _mongoClientProvider; 16 | 17 | protected internal MongoRepository(IMongoClientProvider mongoClientProvider) 18 | { 19 | _mongoClientProvider = mongoClientProvider; 20 | 21 | _jobDbProperties = typeof(JobDb).GetProperties(); 22 | } 23 | 24 | public async Task GetReadyJob(string machineName, TimeSpan obsoleteTime) 25 | { 26 | var collection = _mongoClientProvider.GetCollection(); 27 | 28 | var filter = Builders.Filter.Where(x => 29 | (x.Status == JobStatus.Ready || x.Status == JobStatus.RepeatJob) && x.StartAt < DateTime.UtcNow 30 | || x.Status == JobStatus.Executing && x.StartedExecuting < DateTime.UtcNow - obsoleteTime); 31 | 32 | var update = Builders.Update 33 | .Set(x => x.Status, JobStatus.Executing) 34 | .Set(x => x.ExecutedMachine, machineName) 35 | .Set(x => x.StartedExecuting, DateTime.UtcNow) 36 | .Inc(x => x.CountStarted, 1); 37 | 38 | var options = new FindOneAndUpdateOptions {ReturnDocument = ReturnDocument.After}; 39 | 40 | var result = await collection.FindOneAndUpdateAsync(filter, update, options); 41 | 42 | return result?.ToJobDb(); 43 | } 44 | 45 | public async Task AddJob(JobDb job) 46 | { 47 | var collection = _mongoClientProvider.GetCollection(); 48 | await collection.InsertOneAsync(JobMongoModel.CreateJobMongoModel(job)); 49 | } 50 | 51 | public async Task AddRecurrentJob(JobDb job) 52 | { 53 | var collection = _mongoClientProvider.GetCollection(); 54 | 55 | var update = Builders.Update 56 | .Set(x => x.Cron, job.Cron) 57 | .Set(x => x.StartAt, job.StartAt); 58 | 59 | var needsProperties = _jobDbProperties.Where(x => 60 | x.Name != nameof(JobMongoModel.Cron) && x.Name != nameof(JobMongoModel.StartAt)); 61 | 62 | //Если джоб уже существет апдейтем только 2 поля 63 | //Если нужно создать, то устанавливаем все остальные поля 64 | foreach (var jobDbProperty in needsProperties) 65 | { 66 | update = update.SetOnInsert(jobDbProperty.Name, jobDbProperty.GetValue(job)); 67 | } 68 | 69 | await collection.UpdateOneAsync( 70 | x => x.JobKey == job.JobKey && (x.Status == JobStatus.Executing || x.Status == JobStatus.Ready), 71 | update, 72 | new UpdateOptions 73 | { 74 | IsUpsert = true 75 | }); 76 | } 77 | 78 | public async Task AddRecurrentJobSettings(RecurrentJobSettings settings) 79 | { 80 | var collection = _mongoClientProvider.GetCollection(); 81 | 82 | await collection.ReplaceOneAsync( 83 | x => x.JobKey == settings.JobKey, 84 | RecurrentJobSettingsMongo.Create(settings), 85 | new UpdateOptions 86 | { 87 | IsUpsert = true 88 | }); 89 | } 90 | 91 | public async Task RemoveJob(string jobId) 92 | { 93 | var collection = _mongoClientProvider.GetCollection(); 94 | 95 | await collection.DeleteOneAsync(x => x.JobId == jobId); 96 | } 97 | 98 | public async Task RescheduleRecurrentJob(string jobId, DateTime startAt, Exception error) 99 | { 100 | var collection = _mongoClientProvider.GetCollection(); 101 | 102 | JobMongoModel failedJob = null; 103 | 104 | if (error != null) 105 | { 106 | failedJob = await collection 107 | .Find(Builders.Filter.Where(x => x.JobId == jobId)) 108 | .FirstOrDefaultAsync(); 109 | } 110 | 111 | await collection.UpdateOneAsync( 112 | x => x.JobId == jobId, 113 | Builders.Update 114 | .Set(x => x.StartAt, startAt) 115 | .Set(x => x.Status, JobStatus.Ready)); 116 | 117 | if (error == null) 118 | { 119 | return; 120 | } 121 | 122 | failedJob.JobId = JobBuilderHelpers.GenerateNewJobId(); 123 | failedJob.Status = JobStatus.Failed; 124 | failedJob.Error = error.Message + ' ' + error.StackTrace; 125 | 126 | await collection.InsertOneAsync(failedJob); 127 | } 128 | 129 | public async Task RepeatJob(string jobId, DateTime startAt, Exception error) 130 | { 131 | var collection = _mongoClientProvider.GetCollection(); 132 | 133 | var update = Builders.Update 134 | .Set(x => x.Status, JobStatus.RepeatJob) 135 | .Set(x => x.StartAt, startAt) 136 | .Set(x => x.Error, error.Message + ' ' + error.StackTrace); 137 | 138 | await collection.UpdateOneAsync(x => x.JobId == jobId, update); 139 | } 140 | 141 | public async Task FailedJob(string jobId, Exception error) 142 | { 143 | var collection = _mongoClientProvider.GetCollection(); 144 | 145 | var update = Builders.Update 146 | .Set(x => x.Status, JobStatus.Failed) 147 | .Set(x => x.Error, error.Message + ' ' + error.StackTrace); 148 | 149 | await collection.UpdateOneAsync(x => x.JobId == jobId, update); 150 | } 151 | 152 | public async Task> GetJobStatistic() 153 | { 154 | var collection = _mongoClientProvider.GetCollection(); 155 | var result = await collection.Aggregate() 156 | .Group(x => x.Status, g => new 157 | { 158 | Id = g.Key, 159 | Sum = g.Sum(x => 1) 160 | }) 161 | .ToListAsync(); 162 | 163 | var dict = result.ToDictionary(x => x.Id, x => x.Sum); 164 | 165 | foreach (JobStatus jobStatus in Enum.GetValues(typeof(JobStatus))) 166 | { 167 | if (!dict.ContainsKey(jobStatus)) 168 | { 169 | dict.Add(jobStatus, 0); 170 | } 171 | } 172 | 173 | return dict; 174 | } 175 | } 176 | } -------------------------------------------------------------------------------- /src/Horarium.Mongo/MongoRepositoryFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Horarium.Repository; 3 | using MongoDB.Driver; 4 | 5 | namespace Horarium.Mongo 6 | { 7 | public static class MongoRepositoryFactory 8 | { 9 | public static IJobRepository Create(string connectionString) 10 | { 11 | if (string.IsNullOrEmpty(connectionString)) 12 | throw new ArgumentNullException(nameof(connectionString), "Connection string is empty"); 13 | 14 | var provider = new MongoClientProvider(connectionString); 15 | return new MongoRepository(provider); 16 | } 17 | 18 | public static IJobRepository Create(MongoUrl mongoUrl) 19 | { 20 | if (mongoUrl == null) 21 | throw new ArgumentNullException(nameof(mongoUrl), "mongoUrl is null"); 22 | 23 | var provider = new MongoClientProvider(mongoUrl); 24 | return new MongoRepository(provider); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Horarium.Mongo/RecurrentJobSettingsMongo.cs: -------------------------------------------------------------------------------- 1 | using Horarium.Repository; 2 | using MongoDB.Bson; 3 | using MongoDB.Bson.Serialization.Attributes; 4 | 5 | namespace Horarium.Mongo 6 | { 7 | [MongoEntity("horarium.recurrentJobSettings")] 8 | public class RecurrentJobSettingsMongo 9 | { 10 | public static RecurrentJobSettingsMongo Create(RecurrentJobSettings jobSettings) 11 | { 12 | return new RecurrentJobSettingsMongo 13 | { 14 | JobKey = jobSettings.JobKey, 15 | JobType = jobSettings.JobType, 16 | Cron = jobSettings.Cron 17 | }; 18 | } 19 | 20 | [BsonId] 21 | [BsonRepresentation(BsonType.String)] 22 | public string JobKey { get; private set; } 23 | 24 | [BsonRepresentation(BsonType.String)] 25 | [BsonElement("JobType")] 26 | public string JobType { get; private set; } 27 | 28 | [BsonRepresentation(BsonType.String)] 29 | [BsonElement("Cron")] 30 | public string Cron { get; private set; } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Horarium.Sample/CustomRepeatStrategy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Horarium.Interfaces; 3 | 4 | namespace Horarium.Sample 5 | { 6 | public class CustomRepeatStrategy : IFailedRepeatStrategy { 7 | public TimeSpan GetNextStartInterval(int countStarted) 8 | { 9 | return TimeSpan.FromSeconds(3); 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /src/Horarium.Sample/FailedTestJob.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Horarium.Interfaces; 4 | 5 | namespace Horarium.Sample 6 | { 7 | public class FailedTestJob : IJob 8 | { 9 | public Task Execute(int param) 10 | { 11 | Console.WriteLine($"Failed job executed with param {param}"); 12 | throw new Exception(); 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/Horarium.Sample/FallbackTestJob.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Horarium.Interfaces; 4 | 5 | namespace Horarium.Sample 6 | { 7 | public class FallbackTestJob : IJob 8 | { 9 | public Task Execute(int param) 10 | { 11 | Console.WriteLine($"Fallback job executed with param {param}"); 12 | return Task.CompletedTask; 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/Horarium.Sample/Horarium.Sample.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netcoreapp2.2 4 | Exe 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/Horarium.Sample/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Horarium.Mongo; 5 | 6 | namespace Horarium.Sample 7 | { 8 | static class Program 9 | { 10 | static async Task Main() 11 | { 12 | await Task.Run(StartScheduler); 13 | Console.WriteLine("Start"); 14 | Thread.Sleep(1000000); 15 | Console.ReadKey(); 16 | } 17 | 18 | static async Task StartScheduler() 19 | { 20 | var horarium = 21 | new HorariumServer(MongoRepositoryFactory.Create("mongodb://localhost:27017/schedOpenSource")); 22 | 23 | await horarium.CreateRecurrent(Cron.SecondInterval(10)).Schedule(); 24 | 25 | await new HorariumClient(MongoRepositoryFactory.Create("mongodb://localhost:27017/schedOpenSource")) 26 | .GetJobStatistic(); 27 | 28 | var firstJobDelay = TimeSpan.FromSeconds(20); 29 | 30 | var secondJobDelay = TimeSpan.FromSeconds(15); 31 | 32 | await horarium 33 | .Schedule(1, conf => conf // 1-st job 34 | .WithDelay(firstJobDelay) 35 | .Next(2) // 2-nd job 36 | .WithDelay(secondJobDelay) 37 | .Next(3) // 3-rd job (global obsolete from settings and no delay will be applied) 38 | .Next(4) // 4-th job failed with exception 39 | .AddRepeatStrategy() 40 | .MaxRepeatCount(3) 41 | .AddFallbackConfiguration( 42 | x => x.GoToNextJob()) // execution continues after all attempts 43 | .Next(5) // 5-th job job failed with exception 44 | .MaxRepeatCount(1) 45 | .AddFallbackConfiguration( 46 | x => x.ScheduleFallbackJob(6, builder => 47 | { 48 | builder.Next(7); 49 | })) // 6-th and 7-th jobs executes after all retries 50 | ); 51 | 52 | horarium.Start(); 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /src/Horarium.Sample/TestJob.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Horarium.Interfaces; 4 | 5 | namespace Horarium.Sample 6 | { 7 | public class TestJob : IJob 8 | { 9 | public async Task Execute(int param) 10 | { 11 | Console.WriteLine(param); 12 | await Task.Run(() => { }); 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/Horarium.Sample/TestRecurrentJob.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Horarium.Interfaces; 4 | 5 | namespace Horarium.Sample 6 | { 7 | public class TestRecurrentJob : IJobRecurrent 8 | { 9 | public Task Execute() 10 | { 11 | Console.WriteLine("Run -" + DateTime.Now); 12 | return Task.CompletedTask; 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/Horarium.Test/AdderJobTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Moq; 4 | using Newtonsoft.Json; 5 | using Horarium.Handlers; 6 | using Horarium.Repository; 7 | using Xunit; 8 | 9 | namespace Horarium.Test 10 | { 11 | public class AdderJobTest 12 | { 13 | [Fact] 14 | public async Task AddNewRecurrentJob_Success() 15 | { 16 | // Arrange 17 | var jobRepositoryMock = new Mock(); 18 | 19 | var jobsAdder = new AdderJobs(jobRepositoryMock.Object, new JsonSerializerSettings()); 20 | 21 | var job = new JobMetadata 22 | { 23 | Cron = Cron.SecondInterval(15), 24 | ObsoleteInterval = TimeSpan.FromMinutes(5), 25 | JobType = typeof(TestReccurrentJob), 26 | JobKey = nameof(TestReccurrentJob), 27 | Status = JobStatus.Ready, 28 | JobId = Guid.NewGuid().ToString("N"), 29 | StartAt = DateTime.UtcNow + TimeSpan.FromSeconds(10), 30 | CountStarted = 0 31 | }; 32 | 33 | // Act 34 | await jobsAdder.AddRecurrentJob(job); 35 | 36 | // Assert 37 | jobRepositoryMock.Verify(x => x.AddRecurrentJob(It.Is(j => j.Status == job.Status 38 | && j.CountStarted == job.CountStarted 39 | && j.JobKey == job.JobKey 40 | && j.Cron == job.Cron 41 | && j.JobId == job.JobId 42 | )), Times.Once); 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /src/Horarium.Test/AspNetCore/RegistrationHorariumExtensionTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Horarium.AspNetCore; 4 | using Horarium.Interfaces; 5 | using Horarium.Repository; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Moq; 8 | using Xunit; 9 | 10 | namespace Horarium.Test.AspNetCore 11 | { 12 | public class RegistrationHorariumExtensionTest 13 | { 14 | [Fact] 15 | public void AddHorariumServer_DefaultSettings_ReplaceForAspNetCore() 16 | { 17 | var service = new ServiceCollection(); 18 | 19 | var settings = new HorariumSettings(); 20 | 21 | service.AddHorariumServer(Mock.Of(), 22 | provider => settings); 23 | 24 | var descriptor = service.Single(x => x.ServiceType == typeof(IHorarium)); 25 | var horarium = descriptor.ImplementationFactory(Mock.Of()); 26 | 27 | Assert.Equal(ServiceLifetime.Singleton, descriptor.Lifetime); 28 | Assert.Equal(typeof(IHorarium), descriptor.ServiceType); 29 | Assert.Equal(typeof(JobScopeFactory), settings.JobScopeFactory.GetType()); 30 | Assert.Equal(typeof(HorariumLogger), settings.Logger.GetType()); 31 | Assert.Equal(typeof(HorariumServer), horarium.GetType()); 32 | 33 | Assert.Contains(service, x => x.ImplementationType == typeof(HorariumServerHostedService)); 34 | } 35 | 36 | [Fact] 37 | public void AddHorariumClient_DefaultSettings_ReplaceForAspNetCore() 38 | { 39 | var serviceMock = new Mock(); 40 | 41 | var service = serviceMock.Object; 42 | 43 | ServiceDescriptor descriptor = null; 44 | 45 | var settings = new HorariumSettings(); 46 | 47 | serviceMock.Setup(x => x.Add(It.IsAny())) 48 | .Callback(x => descriptor = x); 49 | 50 | service.AddHorariumClient(Mock.Of(), 51 | provider => settings); 52 | 53 | var horarium = descriptor.ImplementationFactory(Mock.Of()); 54 | 55 | Assert.Equal(ServiceLifetime.Singleton, descriptor.Lifetime); 56 | Assert.Equal(typeof(IHorarium), descriptor.ServiceType); 57 | Assert.Equal(typeof(JobScopeFactory), settings.JobScopeFactory.GetType()); 58 | Assert.Equal(typeof(HorariumLogger), settings.Logger.GetType()); 59 | Assert.Equal(typeof(HorariumClient), horarium.GetType()); 60 | } 61 | 62 | [Fact] 63 | public void AddHorariumClient_CustomSettings_DontReplaceForAspNetCore() 64 | { 65 | var serviceMock = new Mock(); 66 | 67 | var service = serviceMock.Object; 68 | 69 | var settings = new HorariumSettings 70 | { 71 | JobScopeFactory = new JobScopeFactoryTest(), 72 | Logger = new LoggerTest() 73 | }; 74 | 75 | serviceMock.Setup(x => x.Add(It.IsAny())) 76 | .Callback(x => { }); 77 | 78 | service.AddHorariumClient(Mock.Of(), 79 | provider => settings); 80 | 81 | Assert.Equal(typeof(JobScopeFactoryTest), settings.JobScopeFactory.GetType()); 82 | Assert.Equal(typeof(LoggerTest), settings.Logger.GetType()); 83 | } 84 | 85 | [Fact] 86 | public void AddHorariumServer_CustomSettings_DontReplaceForAspNetCore() 87 | { 88 | var serviceMock = new Mock(); 89 | 90 | var service = serviceMock.Object; 91 | 92 | var settings = new HorariumSettings 93 | { 94 | JobScopeFactory = new JobScopeFactoryTest(), 95 | Logger = new LoggerTest() 96 | }; 97 | 98 | serviceMock.Setup(x => x.Add(It.IsAny())) 99 | .Callback(x => { }); 100 | 101 | service.AddHorariumServer(Mock.Of(), 102 | provider => settings); 103 | 104 | Assert.Equal(typeof(JobScopeFactoryTest), settings.JobScopeFactory.GetType()); 105 | Assert.Equal(typeof(LoggerTest), settings.Logger.GetType()); 106 | } 107 | 108 | class JobScopeFactoryTest : IJobScopeFactory 109 | { 110 | public IJobScope Create() 111 | { 112 | throw new NotImplementedException(); 113 | } 114 | } 115 | 116 | class LoggerTest : IHorariumLogger 117 | { 118 | public void Debug(string msg) 119 | { 120 | throw new NotImplementedException(); 121 | } 122 | 123 | public void Debug(Exception ex) 124 | { 125 | throw new NotImplementedException(); 126 | } 127 | 128 | public void Error(Exception ex) 129 | { 130 | throw new NotImplementedException(); 131 | } 132 | 133 | public void Error(string message, Exception ex) 134 | { 135 | throw new NotImplementedException(); 136 | } 137 | } 138 | } 139 | } -------------------------------------------------------------------------------- /src/Horarium.Test/Builders/ParameterizedJobBuilderTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Horarium.Builders.Parameterized; 4 | using Moq; 5 | using Horarium.Interfaces; 6 | using Xunit; 7 | 8 | namespace Horarium.Test.Builders 9 | { 10 | public class ParameterizedJobBuilderTest 11 | { 12 | private readonly Mock _jobsAdderMock = new Mock(); 13 | private readonly TimeSpan _globalObsoleteInterval = TimeSpan.FromMinutes(20); 14 | 15 | [Fact] 16 | public async Task Schedule_NoPropertiesHaveBeenPassed_ShouldScheduleWithDefault() 17 | { 18 | // Arrange 19 | var builder = 20 | new ParameterizedJobBuilder(_jobsAdderMock.Object, "HALLO", _globalObsoleteInterval); 21 | var scheduledJob = new JobMetadata(); 22 | 23 | _jobsAdderMock.Setup(a => a.AddEnqueueJob(It.IsAny())) 24 | .Returns(Task.CompletedTask) 25 | .Callback((JobMetadata job) => scheduledJob = job); 26 | 27 | // Act 28 | await builder.Schedule(); 29 | 30 | // Assert 31 | Assert.Equal(scheduledJob.StartAt, DateTime.UtcNow, TimeSpan.FromMilliseconds(500)); 32 | Assert.Null(scheduledJob.RepeatStrategy); 33 | Assert.Equal(0, scheduledJob.MaxRepeatCount); 34 | _jobsAdderMock.Verify(a => a.AddEnqueueJob(It.IsAny()), Times.Once); 35 | _jobsAdderMock.VerifyNoOtherCalls(); 36 | } 37 | 38 | [Fact] 39 | public async Task Schedule_SetWithDelayAndObsoleteInterval_ShouldScheduleWithDelay() 40 | { 41 | // Arrange 42 | var delay = TimeSpan.FromSeconds(15); 43 | var scheduledJob = new JobMetadata(); 44 | 45 | var builder = 46 | new ParameterizedJobBuilder(_jobsAdderMock.Object, "HALLO", _globalObsoleteInterval) 47 | .WithDelay(delay); 48 | 49 | _jobsAdderMock.Setup(a => a.AddEnqueueJob(It.IsAny())) 50 | .Returns(Task.CompletedTask) 51 | .Callback((JobMetadata job) => scheduledJob = job); 52 | 53 | // Act 54 | await builder.Schedule(); 55 | 56 | // Assert 57 | Assert.Equal(scheduledJob.StartAt, DateTime.UtcNow + delay, TimeSpan.FromMilliseconds(500)); 58 | Assert.Equal(scheduledJob.Delay, delay); 59 | Assert.Equal(scheduledJob.ObsoleteInterval, _globalObsoleteInterval); 60 | _jobsAdderMock.Verify(a => a.AddEnqueueJob(It.IsAny()), Times.Once); 61 | _jobsAdderMock.VerifyNoOtherCalls(); 62 | } 63 | 64 | [Fact] 65 | public async Task Schedule_ManyJobs_ShouldScheduleCorrectly() 66 | { 67 | // Arrange 68 | var builder = new ParameterizedJobBuilder(_jobsAdderMock.Object, "HALLO", 69 | _globalObsoleteInterval); 70 | var scheduledJob = new JobMetadata(); 71 | 72 | var secondJobDelay = TimeSpan.FromSeconds(27); 73 | 74 | var thirdJobDelay = TimeSpan.FromMinutes(20); 75 | 76 | _jobsAdderMock.Setup(a => a.AddEnqueueJob(It.IsAny())) 77 | .Returns(Task.CompletedTask) 78 | .Callback((JobMetadata job) => scheduledJob = job); 79 | 80 | // Act 81 | await builder 82 | .Next("HALLO2") 83 | .WithDelay(secondJobDelay) 84 | .Next("HALLO3") 85 | .WithDelay(thirdJobDelay) 86 | .Schedule(); 87 | 88 | // Assert 89 | var firstJob = scheduledJob; 90 | Assert.Equal(firstJob.ObsoleteInterval, _globalObsoleteInterval); 91 | Assert.Equal(firstJob.Delay, TimeSpan.Zero); 92 | Assert.NotNull(firstJob.NextJob); 93 | 94 | var secondJob = firstJob.NextJob; 95 | Assert.Equal(secondJob.ObsoleteInterval, _globalObsoleteInterval); 96 | Assert.Equal(secondJob.Delay, secondJobDelay); 97 | Assert.NotNull(secondJob.NextJob); 98 | 99 | var thirdJob = secondJob.NextJob; 100 | Assert.Equal(thirdJob.ObsoleteInterval, _globalObsoleteInterval); 101 | Assert.Equal(thirdJob.Delay, thirdJobDelay); 102 | Assert.Null(thirdJob.NextJob); 103 | 104 | _jobsAdderMock.Verify(a => a.AddEnqueueJob(It.IsAny()), Times.Once); 105 | _jobsAdderMock.VerifyNoOtherCalls(); 106 | } 107 | 108 | [Fact] 109 | public async Task Schedule_SetFailedStrategy_SaveInJobMetadata() 110 | { 111 | // Arrange 112 | var scheduledJob = new JobMetadata(); 113 | const int maxRepeatCount = 5; 114 | 115 | var builder = 116 | new ParameterizedJobBuilder(_jobsAdderMock.Object, "HALLO", _globalObsoleteInterval) 117 | .AddRepeatStrategy() 118 | .MaxRepeatCount(maxRepeatCount); 119 | 120 | _jobsAdderMock.Setup(a => a.AddEnqueueJob(It.IsAny())) 121 | .Returns(Task.CompletedTask) 122 | .Callback((JobMetadata job) => scheduledJob = job); 123 | 124 | // Act 125 | await builder.Schedule(); 126 | 127 | // Assert 128 | Assert.Equal(typeof(DefaultRepeatStrategy), scheduledJob.RepeatStrategy); 129 | Assert.Equal(scheduledJob.MaxRepeatCount, maxRepeatCount); 130 | _jobsAdderMock.Verify(a => a.AddEnqueueJob(It.IsAny()), Times.Once); 131 | _jobsAdderMock.VerifyNoOtherCalls(); 132 | } 133 | 134 | [Fact] 135 | public async Task MaxRepeatCountIsZero_ThrowException() 136 | { 137 | // Arrange 138 | const int maxRepeatCount = 0; 139 | 140 | // Act 141 | Assert.Throws(() => 142 | new ParameterizedJobBuilder(_jobsAdderMock.Object, "HALLO", _globalObsoleteInterval) 143 | .MaxRepeatCount(maxRepeatCount)); 144 | 145 | } 146 | } 147 | } -------------------------------------------------------------------------------- /src/Horarium.Test/Builders/RecurrentJobBuilderTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Cronos; 4 | using Moq; 5 | using Horarium.Builders.Recurrent; 6 | using Horarium.Interfaces; 7 | using Xunit; 8 | 9 | namespace Horarium.Test.Builders 10 | { 11 | public class RecurrentJobBuilderTest 12 | { 13 | private readonly Mock _jobsAdderMock = new Mock(); 14 | private static readonly string JobsCron = Cron.SecondInterval(30); 15 | private readonly TimeSpan _globalObsoleteInterval = TimeSpan.FromMinutes(20); 16 | 17 | [Fact] 18 | public async Task Schedule_NoPropertiesHaveBeenSet_ShouldSetDefaultAndSchedule() 19 | { 20 | // Arrange 21 | var builder = new RecurrentJobBuilder(_jobsAdderMock.Object, JobsCron, typeof(TestReccurrentJob), 22 | _globalObsoleteInterval); 23 | var scheduledJob = new JobMetadata(); 24 | 25 | _jobsAdderMock.Setup(a => a.AddRecurrentJob(It.IsAny())) 26 | .Returns(Task.CompletedTask) 27 | .Callback((JobMetadata job) => scheduledJob = job); 28 | 29 | // Act 30 | await builder.Schedule(); 31 | 32 | // Assert 33 | Assert.Equal(scheduledJob.JobKey, typeof(TestReccurrentJob).Name); 34 | _jobsAdderMock.Verify(a => a.AddRecurrentJob(It.IsAny()), Times.Once); 35 | _jobsAdderMock.VerifyNoOtherCalls(); 36 | } 37 | 38 | [Fact] 39 | public async Task Schedule_CorrectCronPassed_ShouldSetRightStartAtAndSchedule() 40 | { 41 | // Arrange 42 | var builder = new RecurrentJobBuilder(_jobsAdderMock.Object, JobsCron, typeof(TestReccurrentJob), 43 | _globalObsoleteInterval); 44 | var scheduledJob = new JobMetadata(); 45 | 46 | var parsedCron = CronExpression.Parse(JobsCron, CronFormat.IncludeSeconds); 47 | var expectedStartAt = parsedCron.GetNextOccurrence(DateTime.UtcNow, TimeZoneInfo.Local); 48 | 49 | _jobsAdderMock.Setup(a => a.AddRecurrentJob(It.IsAny())) 50 | .Returns(Task.CompletedTask) 51 | .Callback((JobMetadata job) => scheduledJob = job); 52 | 53 | // Act 54 | await builder.Schedule(); 55 | 56 | // Assert 57 | Assert.Equal(scheduledJob.StartAt, expectedStartAt); 58 | _jobsAdderMock.Verify(a => a.AddRecurrentJob(It.IsAny()), Times.Once); 59 | _jobsAdderMock.VerifyNoOtherCalls(); 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /src/Horarium.Test/Horarium.Test.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netcoreapp2.2 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Horarium.Test/JobMapperTest.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Horarium.Repository; 3 | using Xunit; 4 | 5 | namespace Horarium.Test 6 | { 7 | public class JobMapperTest 8 | { 9 | private string _strJobType = "Horarium.Test.TestJob, Horarium.Test"; 10 | private string _strJobParamType = "System.String, System.Private.CoreLib"; 11 | private string _strJobParam = @"""test"""; 12 | 13 | [Fact] 14 | public void ToJobDb() 15 | { 16 | var job = new JobMetadata() 17 | { 18 | JobType = typeof(TestJob), 19 | JobParam = "test" 20 | }; 21 | 22 | var jobDb = JobDb.CreatedJobDb(job,new JsonSerializerSettings()); 23 | 24 | Assert.Equal(jobDb.JobType, _strJobType); 25 | Assert.Equal(jobDb.JobParamType, _strJobParamType); 26 | Assert.Equal(jobDb.JobParam, _strJobParam); 27 | } 28 | 29 | [Fact] 30 | public void ToJob() 31 | { 32 | var job = new JobDb 33 | { 34 | JobType = _strJobType, 35 | JobParamType = _strJobParamType, 36 | JobParam = _strJobParam 37 | }; 38 | 39 | var jobDb = job.ToJob(new JsonSerializerSettings()); 40 | 41 | Assert.Equal(typeof(TestJob), jobDb.JobType); 42 | Assert.Equal("test", jobDb.JobParam ); 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /src/Horarium.Test/Mongo/JobMongoModelMapperTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Horarium.Mongo; 3 | using Horarium.Repository; 4 | using Xunit; 5 | 6 | namespace Horarium.Test.Mongo 7 | { 8 | public class JobMongoModelMapperTest 9 | { 10 | [Fact] 11 | public void CreateJobMongoModel_AllFieldsSuccessMap() 12 | { 13 | var jobDb = new JobDb 14 | { 15 | JobType = "Horarium.TestJob, Horarium", 16 | JobParamType = "System.Int32, System.Private.CoreLib", 17 | JobParam = "437", 18 | Status = JobStatus.Ready, 19 | CountStarted = 0, 20 | NextJob = null, 21 | Cron = "* * * * * *", 22 | Delay = TimeSpan.FromSeconds(5) 23 | }; 24 | 25 | var jobMongoModel = JobMongoModel.CreateJobMongoModel(jobDb); 26 | 27 | Assert.Equal("Horarium.TestJob, Horarium", jobMongoModel.JobType); 28 | Assert.Equal("System.Int32, System.Private.CoreLib", jobMongoModel.JobParamType); 29 | Assert.Equal("437", jobMongoModel.JobParam); 30 | Assert.Equal(JobStatus.Ready, jobMongoModel.Status); 31 | Assert.Equal(0, jobMongoModel.CountStarted); 32 | Assert.Null(jobMongoModel.NextJob); 33 | Assert.Equal("* * * * * *", jobMongoModel.Cron); 34 | Assert.Equal(TimeSpan.FromSeconds(5), jobMongoModel.Delay); 35 | } 36 | 37 | [Fact] 38 | public void ToJobDb_AllFieldsSuccessMap() 39 | { 40 | var jobMongoModel = new JobMongoModel 41 | { 42 | JobType = "Horarium.TestJob, Horarium", 43 | JobParamType = "System.Int32, System.Private.CoreLib", 44 | JobParam = "437", 45 | Status = JobStatus.Ready, 46 | CountStarted = 0, 47 | NextJob = null, 48 | Cron = "* * * * * *", 49 | Delay = TimeSpan.FromSeconds(5) 50 | }; 51 | 52 | var jobDb = jobMongoModel.ToJobDb(); 53 | 54 | Assert.Equal("Horarium.TestJob, Horarium", jobDb.JobType); 55 | Assert.Equal("System.Int32, System.Private.CoreLib", jobDb.JobParamType); 56 | Assert.Equal("437", jobDb.JobParam); 57 | Assert.Equal(JobStatus.Ready, jobDb.Status); 58 | Assert.Equal(0, jobDb.CountStarted); 59 | Assert.Null(jobDb.NextJob); 60 | Assert.Equal("* * * * * *", jobDb.Cron); 61 | Assert.Equal(TimeSpan.FromSeconds(5), jobDb.Delay); 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /src/Horarium.Test/Mongo/MongoRepositoryFactoryTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Horarium.Mongo; 4 | using MongoDB.Driver; 5 | using Xunit; 6 | 7 | namespace Horarium.Test.Mongo 8 | { 9 | public class MongoRepositoryFactoryTest 10 | { 11 | [Fact] 12 | public void Create_NullConnectionString_Exception() 13 | { 14 | string connectionString = null; 15 | 16 | Assert.Throws(() => MongoRepositoryFactory.Create(connectionString)); 17 | } 18 | 19 | [Fact] 20 | public void Create_NullMongoUrl_Exception() 21 | { 22 | MongoUrl mongoUrl = null; 23 | 24 | Assert.Throws(() => MongoRepositoryFactory.Create(mongoUrl)); 25 | } 26 | 27 | [Fact] 28 | public async Task Create_WellFormedUrl_AccessMongoLazily() 29 | { 30 | const string stubMongoUrl = "mongodb://fake-url:27017/fake_database_name/?serverSelectionTimeoutMs=100"; 31 | 32 | var mongoRepository = MongoRepositoryFactory.Create(stubMongoUrl); 33 | 34 | await Assert.ThrowsAsync(() => mongoRepository.GetJobStatistic()); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /src/Horarium.Test/RunnerJobTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Moq; 5 | using Newtonsoft.Json; 6 | using Horarium.Handlers; 7 | using Horarium.Interfaces; 8 | using Horarium.Repository; 9 | using Xunit; 10 | 11 | namespace Horarium.Test 12 | { 13 | public class RunnerJobTest 14 | { 15 | [Fact] 16 | public async Task Start_Stop() 17 | { 18 | // Arrange 19 | var jobRepositoryMock = new Mock(); 20 | 21 | jobRepositoryMock.Setup(x => x.GetReadyJob(It.IsAny(), It.IsAny())); 22 | 23 | var runnerJobs = new RunnerJobs(jobRepositoryMock.Object, 24 | new HorariumSettings(), 25 | new JsonSerializerSettings(), 26 | Mock.Of(), 27 | Mock.Of(), 28 | Mock.Of()); 29 | 30 | // Act 31 | runnerJobs.Start(); 32 | 33 | await Task.Delay(TimeSpan.FromSeconds(1)); 34 | 35 | await runnerJobs.Stop(CancellationToken.None); 36 | 37 | jobRepositoryMock.Invocations.Clear(); 38 | 39 | await Task.Delay(TimeSpan.FromSeconds(1)); 40 | 41 | // Assert 42 | jobRepositoryMock.Verify(x => x.GetReadyJob(It.IsAny(), It.IsAny()), Times.Never); 43 | } 44 | 45 | [Fact] 46 | public async Task Start_RecoverAfterIntervalTimeout_AfterFailedDB() 47 | { 48 | // Arrange 49 | var jobRepositoryMock = new Mock(); 50 | 51 | var settings = new HorariumSettings 52 | { 53 | IntervalStartJob = TimeSpan.FromSeconds(2), 54 | }; 55 | 56 | var runnerJobs = new RunnerJobs(jobRepositoryMock.Object, 57 | settings, 58 | new JsonSerializerSettings(), 59 | Mock.Of(), 60 | Mock.Of(), 61 | Mock.Of()); 62 | 63 | jobRepositoryMock.SetupSequence(x => x.GetReadyJob(It.IsAny(), It.IsAny())) 64 | .ThrowsAsync(new Exception()) 65 | .ReturnsAsync(new JobDb()); 66 | 67 | // Act 68 | runnerJobs.Start(); 69 | await Task.Delay(settings.IntervalStartJob + TimeSpan.FromMilliseconds(1000)); 70 | 71 | // Assert 72 | jobRepositoryMock.Verify(r => r.GetReadyJob(It.IsAny(), It.IsAny()), Times.AtLeast(2)); 73 | } 74 | 75 | [Fact] 76 | public async Task Start_WontRecoverBeforeIntervalTimeout_AfterFailedDB() 77 | { 78 | // Arrange 79 | var jobRepositoryMock = new Mock(); 80 | 81 | var settings = new HorariumSettings 82 | { 83 | IntervalStartJob = TimeSpan.FromSeconds(2), 84 | }; 85 | 86 | var runnerJobs = new RunnerJobs(jobRepositoryMock.Object, 87 | settings, 88 | new JsonSerializerSettings(), 89 | Mock.Of(), 90 | Mock.Of(), 91 | Mock.Of()); 92 | 93 | jobRepositoryMock.SetupSequence(x => x.GetReadyJob(It.IsAny(), It.IsAny())) 94 | .ThrowsAsync(new Exception()) 95 | .ReturnsAsync(new JobDb()); 96 | 97 | // Act 98 | runnerJobs.Start(); 99 | await Task.Delay(settings.IntervalStartJob - TimeSpan.FromMilliseconds(500)); 100 | 101 | // Assert 102 | jobRepositoryMock.Verify(r => r.GetReadyJob(It.IsAny(), It.IsAny()), Times.Once); 103 | } 104 | 105 | [Fact] 106 | public async Task Start_ExecutionWithDelay_WithThrottle() 107 | { 108 | // Arrange 109 | var jobRepositoryMock = new Mock(); 110 | 111 | var settings = new HorariumSettings 112 | { 113 | IntervalStartJob = TimeSpan.FromSeconds(1), 114 | JobThrottleSettings = new JobThrottleSettings 115 | { 116 | UseJobThrottle = true, 117 | IntervalMultiplier = 1, 118 | JobRetrievalAttempts = 1 119 | } 120 | }; 121 | 122 | var runnerJobs = new RunnerJobs(jobRepositoryMock.Object, 123 | settings, 124 | new JsonSerializerSettings(), 125 | Mock.Of(), 126 | Mock.Of(), 127 | Mock.Of()); 128 | 129 | // Act 130 | runnerJobs.Start(); 131 | await Task.Delay(settings.IntervalStartJob - TimeSpan.FromMilliseconds(500)); 132 | jobRepositoryMock.Invocations.Clear(); 133 | 134 | await Task.Delay(settings.IntervalStartJob + settings.IntervalStartJob.Multiply(settings.JobThrottleSettings.IntervalMultiplier)); 135 | 136 | // Assert 137 | jobRepositoryMock.Verify(r => r.GetReadyJob(It.IsAny(), It.IsAny()), Times.Once); 138 | } 139 | 140 | [Fact] 141 | public async Task Start_ExecutionWithDelay_IncreaseInterval() 142 | { 143 | // Arrange 144 | var jobRepositoryMock = new Mock(); 145 | 146 | jobRepositoryMock.Setup(x => x.GetReadyJob(It.IsAny(), It.IsAny())) 147 | .ReturnsAsync(() => null); 148 | 149 | var settings = new HorariumSettings 150 | { 151 | IntervalStartJob = TimeSpan.FromSeconds(1), 152 | JobThrottleSettings = new JobThrottleSettings 153 | { 154 | UseJobThrottle = true, 155 | IntervalMultiplier = 1, 156 | JobRetrievalAttempts = 1, 157 | } 158 | }; 159 | 160 | var runnerJobs = new RunnerJobs(jobRepositoryMock.Object, 161 | settings, 162 | new JsonSerializerSettings(), 163 | Mock.Of(), 164 | Mock.Of(), 165 | Mock.Of()); 166 | 167 | // Act 168 | runnerJobs.Start(); 169 | await Task.Delay(settings.IntervalStartJob - TimeSpan.FromMilliseconds(500)); 170 | jobRepositoryMock.Invocations.Clear(); 171 | 172 | var interval = settings.IntervalStartJob + 173 | settings.IntervalStartJob.Multiply(settings.JobThrottleSettings.IntervalMultiplier); 174 | await Task.Delay(interval); 175 | interval += settings.IntervalStartJob.Multiply(settings.JobThrottleSettings.IntervalMultiplier); 176 | await Task.Delay(interval); 177 | interval += settings.IntervalStartJob.Multiply(settings.JobThrottleSettings.IntervalMultiplier); 178 | await Task.Delay(interval); 179 | 180 | // Assert 181 | jobRepositoryMock.Verify(r => r.GetReadyJob(It.IsAny(), It.IsAny()), Times.Exactly(3)); 182 | } 183 | 184 | [Fact] 185 | public async Task Start_ExecutionWithDelay_MaxInterval() 186 | { 187 | // Arrange 188 | var jobRepositoryMock = new Mock(); 189 | 190 | jobRepositoryMock.Setup(x => x.GetReadyJob(It.IsAny(), It.IsAny())) 191 | .ReturnsAsync(() => null); 192 | 193 | var settings = new HorariumSettings 194 | { 195 | IntervalStartJob = TimeSpan.FromSeconds(1), 196 | JobThrottleSettings = new JobThrottleSettings 197 | { 198 | UseJobThrottle = true, 199 | IntervalMultiplier = 1, 200 | JobRetrievalAttempts = 1, 201 | MaxJobThrottleInterval = TimeSpan.FromSeconds(1) 202 | } 203 | }; 204 | 205 | var runnerJobs = new RunnerJobs(jobRepositoryMock.Object, 206 | settings, 207 | new JsonSerializerSettings(), 208 | Mock.Of(), 209 | Mock.Of(), 210 | Mock.Of()); 211 | 212 | // Act 213 | runnerJobs.Start(); 214 | await Task.Delay(settings.IntervalStartJob - TimeSpan.FromMilliseconds(500)); 215 | jobRepositoryMock.Invocations.Clear(); 216 | 217 | await Task.Delay(TimeSpan.FromSeconds(5)); 218 | // Assert 219 | jobRepositoryMock.Verify(r => r.GetReadyJob(It.IsAny(), It.IsAny()), Times.Exactly(5)); 220 | } 221 | 222 | [Fact] 223 | public async Task Start_NextJobStarted_AddsJobTaskToUncompletedTasks() 224 | { 225 | // Arrange 226 | var jobRepositoryMock = new Mock(); 227 | var uncompletedTaskList = new Mock(); 228 | 229 | uncompletedTaskList.Setup(x => x.Add(It.IsAny())); 230 | 231 | jobRepositoryMock.Setup(x => x.GetReadyJob(It.IsAny(), It.IsAny())) 232 | .ReturnsAsync(new JobDb 233 | { 234 | JobType = typeof(object).ToString(), 235 | }); 236 | 237 | var runnerJobs = new RunnerJobs(jobRepositoryMock.Object, 238 | new HorariumSettings 239 | { 240 | IntervalStartJob = TimeSpan.FromHours(1), // prevent second job from starting 241 | }, 242 | new JsonSerializerSettings(), 243 | Mock.Of(), 244 | Mock.Of(), 245 | uncompletedTaskList.Object); 246 | 247 | // Act 248 | runnerJobs.Start(); 249 | await Task.Delay(TimeSpan.FromSeconds(5)); 250 | await runnerJobs.Stop(CancellationToken.None); 251 | 252 | // Assert 253 | uncompletedTaskList.Verify(x=>x.Add(It.IsAny()), Times.Once); 254 | } 255 | 256 | [Fact] 257 | public async Task StopAsync_AwaitsWhenAllCompleted() 258 | { 259 | // Arrange 260 | var jobRepositoryMock = new Mock(); 261 | var uncompletedTaskList = new Mock(); 262 | var cancellationToken = new CancellationTokenSource().Token; 263 | 264 | var settings = new HorariumSettings 265 | { 266 | IntervalStartJob = TimeSpan.FromSeconds(2), 267 | }; 268 | 269 | var runnerJobs = new RunnerJobs(jobRepositoryMock.Object, 270 | settings, 271 | new JsonSerializerSettings(), 272 | Mock.Of(), 273 | Mock.Of(), 274 | uncompletedTaskList.Object); 275 | 276 | jobRepositoryMock.Setup(x => x.GetReadyJob(It.IsAny(), It.IsAny())); 277 | 278 | // Act 279 | runnerJobs.Start(); 280 | await Task.Delay(TimeSpan.FromSeconds(1)); 281 | await runnerJobs.Stop(cancellationToken); 282 | 283 | // Assert 284 | uncompletedTaskList.Verify(x => x.WhenAllCompleted(cancellationToken), Times.Once); 285 | } 286 | } 287 | } -------------------------------------------------------------------------------- /src/Horarium.Test/SchedulerSettingsTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | 4 | namespace Horarium.Test 5 | { 6 | public class SchedulerSettingsTest 7 | { 8 | [Fact] 9 | public void IntervalStartJob_IsValid() 10 | { 11 | var defaultIntervalStartJob = TimeSpan.FromMilliseconds(100); 12 | 13 | var settings = new HorariumSettings(); 14 | 15 | Assert.Equal(defaultIntervalStartJob, settings.IntervalStartJob); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/Horarium.Test/TestJob.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Horarium.Interfaces; 3 | 4 | namespace Horarium.Test 5 | { 6 | public class TestJob : IJob 7 | { 8 | public async Task Execute(string param) 9 | { 10 | await Task.Run(() => { }); 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /src/Horarium.Test/TestRecurrentJob.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Horarium.Interfaces; 3 | 4 | namespace Horarium.Test 5 | { 6 | public class TestReccurrentJob : IJobRecurrent 7 | { 8 | public Task Execute() 9 | { 10 | return Task.CompletedTask; 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /src/Horarium.Test/UncompletedTaskListTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Horarium.Handlers; 5 | using Xunit; 6 | 7 | namespace Horarium.Test 8 | { 9 | public class UncompletedTaskListTests 10 | { 11 | private readonly UncompletedTaskList _uncompletedTaskList = new UncompletedTaskList(); 12 | 13 | [Fact] 14 | public async Task Add_TaskWithAnyResult_KeepsTaskUntilCompleted() 15 | { 16 | var tcs1 = new TaskCompletionSource(); 17 | var tcs2 = new TaskCompletionSource(); 18 | var tcs3 = new TaskCompletionSource(); 19 | 20 | _uncompletedTaskList.Add(tcs1.Task); 21 | _uncompletedTaskList.Add(tcs2.Task); 22 | _uncompletedTaskList.Add(tcs3.Task); 23 | 24 | Assert.Equal(3, _uncompletedTaskList.Count); 25 | 26 | tcs1.SetResult(false); 27 | await Task.Delay(TimeSpan.FromSeconds(1)); // give a chance to finish continuations 28 | Assert.Equal(2, _uncompletedTaskList.Count); 29 | 30 | tcs2.SetException(new ApplicationException()); 31 | await Task.Delay(TimeSpan.FromSeconds(1)); 32 | Assert.Equal(1, _uncompletedTaskList.Count); 33 | 34 | tcs3.SetCanceled(); 35 | await Task.Delay(TimeSpan.FromSeconds(1)); 36 | Assert.Equal(0, _uncompletedTaskList.Count); 37 | } 38 | 39 | [Fact] 40 | public async Task WhenAllCompleted_NoTasks_ReturnsCompletedTask() 41 | { 42 | // Act 43 | var whenAll = _uncompletedTaskList.WhenAllCompleted(CancellationToken.None); 44 | 45 | // Assert 46 | Assert.True(whenAll.IsCompletedSuccessfully); 47 | await whenAll; 48 | } 49 | 50 | [Fact] 51 | public async Task WhenAllCompleted_TaskNotCompleted_AwaitsUntilTaskCompleted() 52 | { 53 | // Arrange 54 | var tcs = new TaskCompletionSource(); 55 | _uncompletedTaskList.Add(tcs.Task); 56 | 57 | // Act 58 | var whenAll = _uncompletedTaskList.WhenAllCompleted(CancellationToken.None); 59 | 60 | // Assert 61 | await Task.Delay(TimeSpan.FromSeconds(1)); // give a chance to finish any running tasks 62 | Assert.False(whenAll.IsCompleted); 63 | 64 | tcs.SetResult(false); 65 | await Task.Delay(TimeSpan.FromSeconds(1)); 66 | Assert.True(whenAll.IsCompletedSuccessfully); 67 | 68 | await whenAll; 69 | } 70 | 71 | [Fact] 72 | public async Task WhenAllCompleted_TaskFaulted_DoesNotThrow() 73 | { 74 | // Arrange 75 | _uncompletedTaskList.Add(Task.FromException(new ApplicationException())); 76 | 77 | // Act 78 | var whenAll = _uncompletedTaskList.WhenAllCompleted(CancellationToken.None); 79 | 80 | await whenAll; 81 | } 82 | 83 | [Fact] 84 | public async Task WhenAllCompleted_CancellationRequested_DoesNotAwait_ThrowsOperationCancelledException() 85 | { 86 | // Arrange 87 | var tcs = new TaskCompletionSource(); 88 | var cts = new CancellationTokenSource(); 89 | _uncompletedTaskList.Add(tcs.Task); 90 | 91 | // Act 92 | var whenAll = _uncompletedTaskList.WhenAllCompleted(cts.Token); 93 | 94 | // Assert 95 | cts.Cancel(); 96 | await Task.Delay(TimeSpan.FromSeconds(1), CancellationToken.None); // give a chance to finish any running tasks 97 | 98 | var exception = await Assert.ThrowsAsync(() => whenAll); 99 | Assert.Equal(cts.Token, exception.CancellationToken); 100 | } 101 | } 102 | } -------------------------------------------------------------------------------- /src/Horarium.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26403.7 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Horarium", "Horarium\Horarium.csproj", "{6F282E69-0744-4652-8458-090F66295357}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Horarium.Test", "Horarium.Test\Horarium.Test.csproj", "{4808B381-8AA8-42E8-AE78-7E8E1BC46947}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Horarium.IntegrationTest", "Horarium.IntegrationTest\Horarium.IntegrationTest.csproj", "{90957511-9034-46CD-A5B8-4383AA8FA1E7}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Horarium.Sample", "Horarium.Sample\Horarium.Sample.csproj", "{79D8507C-FD7C-4114-8355-140D5BDA44F5}" 13 | EndProject 14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Horarium.Mongo", "Horarium.Mongo\Horarium.Mongo.csproj", "{0C8FC2A8-2E7D-4400-8D7B-C5F6FDB110D5}" 15 | EndProject 16 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Horarium.AspNetCore", "Horarium.AspNetCore\Horarium.AspNetCore.csproj", "{BDA3804A-15B5-46A3-8F09-FD991FAAF1D0}" 17 | EndProject 18 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Horarium.InMemory", "Horarium.InMemory\Horarium.InMemory.csproj", "{567A7F77-22BB-43D0-AE4B-AF929E7FD826}" 19 | EndProject 20 | Global 21 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 22 | Debug|Any CPU = Debug|Any CPU 23 | Release|Any CPU = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 26 | {6F282E69-0744-4652-8458-090F66295357}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {6F282E69-0744-4652-8458-090F66295357}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {6F282E69-0744-4652-8458-090F66295357}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {6F282E69-0744-4652-8458-090F66295357}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {4808B381-8AA8-42E8-AE78-7E8E1BC46947}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {4808B381-8AA8-42E8-AE78-7E8E1BC46947}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {4808B381-8AA8-42E8-AE78-7E8E1BC46947}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {4808B381-8AA8-42E8-AE78-7E8E1BC46947}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {90957511-9034-46CD-A5B8-4383AA8FA1E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {90957511-9034-46CD-A5B8-4383AA8FA1E7}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {90957511-9034-46CD-A5B8-4383AA8FA1E7}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {90957511-9034-46CD-A5B8-4383AA8FA1E7}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {79D8507C-FD7C-4114-8355-140D5BDA44F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {79D8507C-FD7C-4114-8355-140D5BDA44F5}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {79D8507C-FD7C-4114-8355-140D5BDA44F5}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {79D8507C-FD7C-4114-8355-140D5BDA44F5}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {0C8FC2A8-2E7D-4400-8D7B-C5F6FDB110D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {0C8FC2A8-2E7D-4400-8D7B-C5F6FDB110D5}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {0C8FC2A8-2E7D-4400-8D7B-C5F6FDB110D5}.Release|Any CPU.ActiveCfg = Release|Any CPU 45 | {0C8FC2A8-2E7D-4400-8D7B-C5F6FDB110D5}.Release|Any CPU.Build.0 = Release|Any CPU 46 | {BDA3804A-15B5-46A3-8F09-FD991FAAF1D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {BDA3804A-15B5-46A3-8F09-FD991FAAF1D0}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {BDA3804A-15B5-46A3-8F09-FD991FAAF1D0}.Release|Any CPU.ActiveCfg = Release|Any CPU 49 | {BDA3804A-15B5-46A3-8F09-FD991FAAF1D0}.Release|Any CPU.Build.0 = Release|Any CPU 50 | {567A7F77-22BB-43D0-AE4B-AF929E7FD826}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 51 | {567A7F77-22BB-43D0-AE4B-AF929E7FD826}.Debug|Any CPU.Build.0 = Debug|Any CPU 52 | {567A7F77-22BB-43D0-AE4B-AF929E7FD826}.Release|Any CPU.ActiveCfg = Release|Any CPU 53 | {567A7F77-22BB-43D0-AE4B-AF929E7FD826}.Release|Any CPU.Build.0 = Release|Any CPU 54 | EndGlobalSection 55 | GlobalSection(SolutionProperties) = preSolution 56 | HideSolutionNode = FALSE 57 | EndGlobalSection 58 | EndGlobal 59 | -------------------------------------------------------------------------------- /src/Horarium/Builders/IDelayedJobBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Horarium.Builders 4 | { 5 | [Obsolete("use IJobSequenceBuilder instead")] 6 | public interface IDelayedJobBuilder where TJobBuilder : IJobBuilder 7 | { 8 | /// 9 | /// Set delay for start this job 10 | /// 11 | /// 12 | /// 13 | TJobBuilder WithDelay(TimeSpan delay); 14 | } 15 | } -------------------------------------------------------------------------------- /src/Horarium/Builders/IJobBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace Horarium.Builders 5 | { 6 | [Obsolete("use IJobSequenceBuilder instead")] 7 | public interface IJobBuilder 8 | { 9 | /// 10 | /// Run current job 11 | /// 12 | /// 13 | Task Schedule(); 14 | } 15 | } -------------------------------------------------------------------------------- /src/Horarium/Builders/JobBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace Horarium.Builders 5 | { 6 | internal abstract class JobBuilder : IJobBuilder 7 | { 8 | protected JobMetadata Job; 9 | 10 | protected JobBuilder(Type jobType) 11 | { 12 | GenerateNewJob(jobType); 13 | } 14 | 15 | public abstract Task Schedule(); 16 | 17 | protected void GenerateNewJob() 18 | { 19 | GenerateNewJob(typeof(TJob)); 20 | } 21 | 22 | private void GenerateNewJob(Type jobType) 23 | { 24 | Job = JobBuilderHelpers.GenerateNewJob(jobType); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/Horarium/Builders/JobBuilderHelpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Horarium.Builders 6 | { 7 | public static class JobBuilderHelpers 8 | { 9 | public static JobMetadata GenerateNewJob(Type jobType) 10 | { 11 | return new JobMetadata 12 | { 13 | JobId = Guid.NewGuid().ToString("N"), 14 | JobType = jobType, 15 | Status = JobStatus.Ready, 16 | CountStarted = 0 17 | }; 18 | } 19 | 20 | public static string GenerateNewJobId() => Guid.NewGuid().ToString("N"); 21 | 22 | public static JobMetadata BuildJobsSequence(Queue jobsQueue, TimeSpan globalObsoleteInterval) 23 | { 24 | var job = jobsQueue.Dequeue(); 25 | 26 | FillWithDefaultIfNecessary(job, globalObsoleteInterval); 27 | var previous = job; 28 | 29 | while (jobsQueue.Any()) 30 | { 31 | previous.NextJob = jobsQueue.Dequeue(); 32 | previous = previous.NextJob; 33 | FillWithDefaultIfNecessary(previous, globalObsoleteInterval); 34 | } 35 | 36 | return job; 37 | } 38 | 39 | private static void FillWithDefaultIfNecessary(JobMetadata job, TimeSpan globalObsoleteInterval) 40 | { 41 | job.Delay = job.Delay ?? TimeSpan.Zero; 42 | job.StartAt = DateTime.UtcNow + job.Delay.Value; 43 | 44 | job.ObsoleteInterval = job.ObsoleteInterval == default(TimeSpan) 45 | ? globalObsoleteInterval 46 | : job.ObsoleteInterval; 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src/Horarium/Builders/JobSequenceBuilder/IJobSequenceBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Horarium.Fallbacks; 3 | using Horarium.Interfaces; 4 | 5 | namespace Horarium.Builders.JobSequenceBuilder 6 | { 7 | public interface IJobSequenceBuilder 8 | { 9 | /// 10 | /// Create next job, it run after previous job 11 | /// 12 | /// 13 | /// 14 | /// 15 | /// 16 | IJobSequenceBuilder Next(TJobParam parameters) where TJob : IJob; 17 | 18 | /// 19 | /// Add custom failed repeat strategy for job 20 | /// 21 | /// 22 | /// 23 | IJobSequenceBuilder AddRepeatStrategy() where TRepeat : IFailedRepeatStrategy; 24 | 25 | /// 26 | /// Set custom max failed repeat count 27 | /// 28 | /// min value is 1, it's mean this job start only one time 29 | /// 30 | IJobSequenceBuilder MaxRepeatCount(int count); 31 | 32 | /// 33 | /// Add custom fallback configuration for job 34 | /// 35 | /// 36 | /// 37 | IJobSequenceBuilder AddFallbackConfiguration(Action configure); 38 | 39 | /// 40 | /// Set delay for start this job 41 | /// 42 | /// 43 | /// 44 | IJobSequenceBuilder WithDelay(TimeSpan delay); 45 | } 46 | } -------------------------------------------------------------------------------- /src/Horarium/Builders/JobSequenceBuilder/JobSequenceBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Horarium.Fallbacks; 4 | using Horarium.Interfaces; 5 | 6 | namespace Horarium.Builders.JobSequenceBuilder 7 | { 8 | internal class JobSequenceBuilder : IJobSequenceBuilder where TJob : IJob 9 | { 10 | private readonly Queue _jobsQueue = new Queue(); 11 | private readonly TimeSpan _globalObsoleteInterval; 12 | 13 | private JobMetadata _job; 14 | 15 | internal JobSequenceBuilder(TJobParam parameters, TimeSpan globalObsoleteInterval) 16 | { 17 | _globalObsoleteInterval = globalObsoleteInterval; 18 | 19 | _job = JobBuilderHelpers.GenerateNewJob(typeof(TJob)); 20 | _job.ObsoleteInterval = globalObsoleteInterval; 21 | _job.JobParam = parameters; 22 | 23 | _jobsQueue.Enqueue(_job); 24 | } 25 | 26 | public IJobSequenceBuilder WithDelay(TimeSpan delay) 27 | { 28 | _job.Delay = delay; 29 | return this; 30 | } 31 | 32 | public IJobSequenceBuilder AddFallbackConfiguration(Action configure) 33 | { 34 | var options = new FallbackStrategyOptions(_globalObsoleteInterval); 35 | if (configure == null) 36 | { 37 | return this; 38 | } 39 | configure(options); 40 | 41 | _job.FallbackStrategyType = options.FallbackStrategyType; 42 | _job.FallbackJob = options.FallbackJobMetadata; 43 | 44 | return this; 45 | } 46 | 47 | public IJobSequenceBuilder Next(TNextJobParam parameters) 48 | where TNextJob : IJob 49 | { 50 | _job = JobBuilderHelpers.GenerateNewJob(typeof(TNextJob)); 51 | _job.JobParam = parameters; 52 | 53 | _jobsQueue.Enqueue(_job); 54 | 55 | return this; 56 | } 57 | 58 | public IJobSequenceBuilder AddRepeatStrategy() where TRepeat : IFailedRepeatStrategy 59 | { 60 | _job.RepeatStrategy = typeof(TRepeat); 61 | return this; 62 | } 63 | 64 | public IJobSequenceBuilder MaxRepeatCount(int count) 65 | { 66 | if (count < 1) 67 | { 68 | throw new ArgumentOutOfRangeException(nameof(count),"min value is 1"); 69 | } 70 | _job.MaxRepeatCount = count; 71 | return this; 72 | } 73 | 74 | public JobMetadata Build() 75 | { 76 | return JobBuilderHelpers.BuildJobsSequence(_jobsQueue, _globalObsoleteInterval); 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /src/Horarium/Builders/Parameterized/IParameterizedJobBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Horarium.Interfaces; 3 | 4 | namespace Horarium.Builders.Parameterized 5 | { 6 | [Obsolete("use IJobSequenceBuilder instead")] 7 | public interface IParameterizedJobBuilder : IJobBuilder, IDelayedJobBuilder 8 | { 9 | /// 10 | /// Create next job, it run after previous job 11 | /// 12 | /// 13 | /// 14 | /// 15 | /// 16 | IParameterizedJobBuilder Next(TJobParam parameters) where TJob : IJob; 17 | 18 | /// 19 | /// Add custom failed repeat strategy for job 20 | /// 21 | /// 22 | /// 23 | IParameterizedJobBuilder AddRepeatStrategy() where TRepeat : IFailedRepeatStrategy; 24 | 25 | /// 26 | /// Set custom max failed repeat count 27 | /// 28 | /// min value is 1, it's mean this job start only one time 29 | /// 30 | IParameterizedJobBuilder MaxRepeatCount(int count); 31 | } 32 | } -------------------------------------------------------------------------------- /src/Horarium/Builders/Parameterized/ParameterizedJobBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Horarium.Interfaces; 5 | 6 | namespace Horarium.Builders.Parameterized 7 | { 8 | [Obsolete("use JobSequenceBuilder instead")] 9 | internal class ParameterizedJobBuilder : JobBuilder, IParameterizedJobBuilder 10 | where TJob : IJob 11 | { 12 | private readonly IAdderJobs _adderJobs; 13 | private readonly TimeSpan _globalObsoleteInterval; 14 | private readonly Queue _jobsQueue = new Queue(); 15 | 16 | internal ParameterizedJobBuilder(IAdderJobs adderJobs, TJobParam parameters, TimeSpan globalObsoleteInterval) 17 | : base(typeof(TJob)) 18 | { 19 | _adderJobs = adderJobs; 20 | _globalObsoleteInterval = globalObsoleteInterval; 21 | Job.ObsoleteInterval = globalObsoleteInterval; 22 | 23 | Job.JobParam = parameters; 24 | 25 | _jobsQueue.Enqueue(Job); 26 | } 27 | 28 | public IParameterizedJobBuilder WithDelay(TimeSpan delay) 29 | { 30 | Job.Delay = delay; 31 | return this; 32 | } 33 | 34 | public IParameterizedJobBuilder ObsoleteAfter(TimeSpan obsoleteInterval) 35 | { 36 | Job.ObsoleteInterval = obsoleteInterval; 37 | return this; 38 | } 39 | 40 | public IParameterizedJobBuilder Next(TNextJobParam parameters) 41 | where TNextJob : IJob 42 | { 43 | GenerateNewJob(); 44 | Job.JobParam = parameters; 45 | 46 | _jobsQueue.Enqueue(Job); 47 | 48 | return this; 49 | } 50 | 51 | public IParameterizedJobBuilder AddRepeatStrategy() where TRepeat : IFailedRepeatStrategy 52 | { 53 | Job.RepeatStrategy = typeof(TRepeat); 54 | return this; 55 | } 56 | 57 | public IParameterizedJobBuilder MaxRepeatCount(int count) 58 | { 59 | if (count < 1) 60 | { 61 | throw new ArgumentOutOfRangeException(nameof(count),"min value is 1"); 62 | } 63 | Job.MaxRepeatCount = count; 64 | return this; 65 | } 66 | 67 | public override Task Schedule() 68 | { 69 | var job = JobBuilderHelpers.BuildJobsSequence(_jobsQueue, _globalObsoleteInterval); 70 | 71 | return _adderJobs.AddEnqueueJob(job); 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /src/Horarium/Builders/Recurrent/IRecurrentJobBuilder.cs: -------------------------------------------------------------------------------- 1 | namespace Horarium.Builders.Recurrent 2 | { 3 | public interface IRecurrentJobBuilder : IJobBuilder 4 | { 5 | /// 6 | /// Add special key(unique identity for recurrent job), default is class name 7 | /// 8 | /// 9 | /// 10 | IRecurrentJobBuilder WithKey(string jobKey); 11 | } 12 | } -------------------------------------------------------------------------------- /src/Horarium/Builders/Recurrent/RecurrentJobBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Horarium.Interfaces; 4 | 5 | namespace Horarium.Builders.Recurrent 6 | { 7 | internal class RecurrentJobBuilder : JobBuilder, IRecurrentJobBuilder 8 | { 9 | private readonly IAdderJobs _adderJobs; 10 | 11 | public RecurrentJobBuilder(IAdderJobs adderJobs, string cron, Type jobType, TimeSpan obsoleteInterval) 12 | : base(jobType) 13 | { 14 | _adderJobs = adderJobs; 15 | Job.ObsoleteInterval = obsoleteInterval; 16 | 17 | Job.Cron = cron; 18 | } 19 | 20 | public IRecurrentJobBuilder WithKey(string jobKey) 21 | { 22 | Job.JobKey = jobKey; 23 | return this; 24 | } 25 | 26 | public override Task Schedule() 27 | { 28 | var nextOccurence = Utils.ParseAndGetNextOccurrence(Job.Cron); 29 | 30 | if (!nextOccurence.HasValue) 31 | { 32 | return Task.CompletedTask; 33 | } 34 | 35 | Job.StartAt = nextOccurence.Value; 36 | Job.JobKey = Job.JobKey ?? Job.JobType.Name; 37 | 38 | return _adderJobs.AddRecurrentJob(Job); 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /src/Horarium/Cron.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Horarium 4 | { 5 | public static class Cron 6 | { 7 | public static string Secondly() 8 | { 9 | return "* * * * * *"; 10 | } 11 | 12 | public static string Minutely() 13 | { 14 | return "0 * * * * *"; 15 | } 16 | 17 | public static string Hourly() 18 | { 19 | return Hourly(minute: 0); 20 | } 21 | 22 | public static string Hourly(int minute) 23 | { 24 | return $"0 {minute} * * * *"; 25 | } 26 | 27 | public static string Daily() 28 | { 29 | return Daily(hour: 0); 30 | } 31 | 32 | public static string Daily(int hour) 33 | { 34 | return Daily(hour, minute: 0); 35 | } 36 | 37 | public static string Daily(int hour, int minute) 38 | { 39 | return $"0 {minute} {hour} * * *"; 40 | } 41 | 42 | public static string Weekly() 43 | { 44 | return Weekly(DayOfWeek.Monday); 45 | } 46 | 47 | public static string Weekly(DayOfWeek dayOfWeek) 48 | { 49 | return Weekly(dayOfWeek, hour: 0); 50 | } 51 | 52 | public static string Weekly(DayOfWeek dayOfWeek, int hour) 53 | { 54 | return Weekly(dayOfWeek, hour, minute: 0); 55 | } 56 | 57 | public static string Weekly(DayOfWeek dayOfWeek, int hour, int minute) 58 | { 59 | return $"0 {minute} {hour} * * {(int) dayOfWeek}"; 60 | } 61 | 62 | public static string Monthly() 63 | { 64 | return Monthly(day: 1); 65 | } 66 | 67 | public static string Monthly(int day) 68 | { 69 | return Monthly(day, hour: 0); 70 | } 71 | 72 | public static string Monthly(int day, int hour) 73 | { 74 | return Monthly(day, hour, minute: 0); 75 | } 76 | 77 | public static string Monthly(int day, int hour, int minute) 78 | { 79 | return $"0 {minute} {hour} {day} * *"; 80 | } 81 | 82 | public static string MinuteInterval(int interval) 83 | { 84 | return $"0 */{interval} * * * *"; 85 | } 86 | 87 | public static string HourInterval(int interval) 88 | { 89 | return $"0 0 */{interval} * * *"; 90 | } 91 | 92 | public static string DayInterval(int interval) 93 | { 94 | return $"0 0 0 */{interval} * *"; 95 | } 96 | 97 | public static string MonthInterval(int interval) 98 | { 99 | return $"0 0 0 1 */{interval} *"; 100 | } 101 | 102 | public static string SecondInterval(int interval) 103 | { 104 | return $"*/{interval} * * * * *"; 105 | } 106 | } 107 | } -------------------------------------------------------------------------------- /src/Horarium/DefaultJobFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Horarium.Interfaces; 3 | 4 | namespace Horarium 5 | { 6 | public class DefaultJobScopeFactory : IJobScopeFactory 7 | { 8 | public IJobScope Create() 9 | { 10 | return new DefaultJobScope(); 11 | } 12 | 13 | public class DefaultJobScope : IJobScope 14 | { 15 | public object CreateJob(Type type) 16 | { 17 | return Activator.CreateInstance(type); 18 | } 19 | 20 | public void Dispose() 21 | { 22 | } 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/Horarium/DefaultRepeatStrategy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Horarium.Interfaces; 3 | 4 | namespace Horarium 5 | { 6 | public class DefaultRepeatStrategy :IFailedRepeatStrategy 7 | { 8 | public TimeSpan GetNextStartInterval(int countStarted) 9 | { 10 | const int increaseRepeat = 10; 11 | 12 | return TimeSpan.FromMinutes(increaseRepeat * countStarted); 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/Horarium/EmptyLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Horarium.Interfaces; 3 | 4 | namespace Horarium 5 | { 6 | public class EmptyLogger : IHorariumLogger 7 | { 8 | public void Debug(string msg) 9 | { 10 | } 11 | 12 | public void Debug(Exception ex) 13 | { 14 | } 15 | 16 | public void Error(Exception ex) 17 | { 18 | } 19 | 20 | public void Error(string message, Exception ex) 21 | { 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/Horarium/Fallbacks/FallbackStrategyOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Horarium.Builders.JobSequenceBuilder; 3 | using Horarium.Interfaces; 4 | 5 | namespace Horarium.Fallbacks 6 | { 7 | internal class FallbackStrategyOptions : IFallbackStrategyOptions 8 | { 9 | private readonly TimeSpan _globalObsoleteInterval; 10 | 11 | public FallbackStrategyTypeEnum? FallbackStrategyType { get; private set; } 12 | public JobMetadata FallbackJobMetadata { get; private set; } 13 | 14 | public FallbackStrategyOptions(TimeSpan globalObsoleteInterval) 15 | { 16 | _globalObsoleteInterval = globalObsoleteInterval; 17 | } 18 | 19 | public void ScheduleFallbackJob(TJobParam parameters, Action fallbackJobConfigure = null) where TJob : IJob 20 | { 21 | FallbackStrategyType = FallbackStrategyTypeEnum.ScheduleFallbackJob; 22 | 23 | var builder = new JobSequenceBuilder(parameters, _globalObsoleteInterval); 24 | fallbackJobConfigure?.Invoke(builder); 25 | 26 | FallbackJobMetadata = builder.Build(); 27 | } 28 | 29 | public void StopExecution() 30 | { 31 | FallbackStrategyType = FallbackStrategyTypeEnum.StopExecution; 32 | } 33 | 34 | public void GoToNextJob() 35 | { 36 | FallbackStrategyType = FallbackStrategyTypeEnum.GoToNextJob; 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/Horarium/Fallbacks/FallbackStrategyTypeEnum.cs: -------------------------------------------------------------------------------- 1 | namespace Horarium.Fallbacks 2 | { 3 | public enum FallbackStrategyTypeEnum 4 | { 5 | StopExecution = 0, 6 | GoToNextJob = 1, 7 | ScheduleFallbackJob = 2, 8 | } 9 | } -------------------------------------------------------------------------------- /src/Horarium/Fallbacks/IFallbackStrategyOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Horarium.Builders.JobSequenceBuilder; 3 | using Horarium.Interfaces; 4 | 5 | namespace Horarium.Fallbacks 6 | { 7 | public interface IFallbackStrategyOptions 8 | { 9 | void ScheduleFallbackJob(TJobParam parameters, Action fallbackJobConfigure = null) 10 | where TJob : IJob; 11 | 12 | void StopExecution(); 13 | 14 | void GoToNextJob(); 15 | } 16 | } -------------------------------------------------------------------------------- /src/Horarium/Handlers/AdderJobs.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Horarium.Interfaces; 3 | using Horarium.Repository; 4 | using Newtonsoft.Json; 5 | 6 | namespace Horarium.Handlers 7 | { 8 | public class AdderJobs : IAdderJobs 9 | { 10 | private readonly IJobRepository _jobRepository; 11 | private readonly JsonSerializerSettings _jsonSerializerSettings; 12 | private readonly IRecurrentJobSettingsAdder _recurrentJobSettingsAdder; 13 | 14 | public AdderJobs(IJobRepository jobRepository, JsonSerializerSettings jsonSerializerSettings) 15 | { 16 | _jobRepository = jobRepository; 17 | _jsonSerializerSettings = jsonSerializerSettings; 18 | _recurrentJobSettingsAdder = new RecurrentJobSettingsAdder(_jobRepository, _jsonSerializerSettings); 19 | } 20 | 21 | public Task AddEnqueueJob(JobMetadata jobMetadata) 22 | { 23 | var job = JobDb.CreatedJobDb(jobMetadata, _jsonSerializerSettings); 24 | 25 | return _jobRepository.AddJob(job); 26 | } 27 | 28 | public async Task AddRecurrentJob(JobMetadata jobMetadata) 29 | { 30 | await _recurrentJobSettingsAdder.Add(jobMetadata.Cron, jobMetadata.JobType, jobMetadata.JobKey); 31 | 32 | await _jobRepository.AddRecurrentJob(JobDb.CreatedJobDb(jobMetadata, _jsonSerializerSettings)); 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /src/Horarium/Handlers/ExecutorJob.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Reflection; 4 | using System.Threading.Tasks; 5 | using Horarium.Interfaces; 6 | using Horarium.Repository; 7 | using Horarium.Fallbacks; 8 | 9 | namespace Horarium.Handlers 10 | { 11 | public class ExecutorJob : IExecutorJob 12 | { 13 | private readonly IJobRepository _jobRepository; 14 | private readonly HorariumSettings _settings; 15 | 16 | public ExecutorJob( 17 | IJobRepository jobRepository, 18 | HorariumSettings settings) 19 | { 20 | _jobRepository = jobRepository; 21 | _settings = settings; 22 | } 23 | 24 | public Task Execute(JobMetadata jobMetadata) 25 | { 26 | if (jobMetadata.JobType.GetTypeInfo().GetInterfaces().Contains(typeof(IJobRecurrent))) 27 | { 28 | return ExecuteJobRecurrent(jobMetadata); 29 | } 30 | 31 | return ExecuteJob(jobMetadata); 32 | } 33 | 34 | private async Task ExecuteJob(JobMetadata jobMetadata) 35 | { 36 | dynamic jobImplementation = null; 37 | 38 | try 39 | { 40 | using (var scope = _settings.JobScopeFactory.Create()) 41 | { 42 | try 43 | { 44 | jobImplementation = scope.CreateJob(jobMetadata.JobType); 45 | } 46 | catch (Exception ex) 47 | { 48 | //Дополнительное логирование ошибки, когда джоб не может быть создан 49 | _settings.Logger.Error($"Ошибка создания джоба {jobMetadata.JobType}", ex); 50 | throw; 51 | } 52 | 53 | _settings.Logger.Debug("got jobImplementation -" + jobImplementation.GetType()); 54 | _settings.Logger.Debug("got JobParam -" + jobMetadata.JobParam.GetType()); 55 | await jobImplementation.Execute((dynamic) jobMetadata.JobParam); 56 | 57 | _settings.Logger.Debug("jobMetadata executed"); 58 | 59 | await ScheduleNextJobIfExists(jobMetadata); 60 | 61 | await _jobRepository.RemoveJob(jobMetadata.JobId); 62 | 63 | _settings.Logger.Debug("jobMetadata saved success"); 64 | } 65 | } 66 | catch (Exception ex) 67 | { 68 | await HandleFailed(jobMetadata, ex, jobImplementation); 69 | } 70 | } 71 | 72 | private async Task ExecuteJobRecurrent(JobMetadata jobMetadata) 73 | { 74 | try 75 | { 76 | using (var scope = _settings.JobScopeFactory.Create()) 77 | { 78 | dynamic jobImplementation; 79 | try 80 | { 81 | jobImplementation = scope.CreateJob(jobMetadata.JobType); 82 | } 83 | catch (Exception ex) 84 | { 85 | //Дополнительное логирование ошибки, когда джоб не может быть создан 86 | _settings.Logger.Error($"Ошибка создания джоба {jobMetadata.JobType}", ex); 87 | throw; 88 | } 89 | 90 | _settings.Logger.Debug("got jobImplementation -" + jobImplementation.GetType()); 91 | 92 | await jobImplementation.Execute(); 93 | 94 | _settings.Logger.Debug("jobMetadata excecuted"); 95 | 96 | await ScheduleNextRecurrentIfPossible(jobMetadata, null); 97 | 98 | _settings.Logger.Debug("jobMetadata saved success"); 99 | } 100 | } 101 | catch (Exception ex) 102 | { 103 | await ScheduleNextRecurrentIfPossible(jobMetadata, ex); 104 | } 105 | } 106 | 107 | private async Task HandleFailed(JobMetadata jobMetadata, Exception ex, dynamic jobImplementation) 108 | { 109 | _settings.Logger.Debug(ex); 110 | 111 | var maxRepeatCount = 112 | jobMetadata.MaxRepeatCount != 0 ? jobMetadata.MaxRepeatCount : _settings.MaxRepeatCount; 113 | 114 | if (jobMetadata.CountStarted >= maxRepeatCount) 115 | { 116 | if (jobImplementation != null && jobImplementation is IAllRepeatesIsFailed) 117 | { 118 | try 119 | { 120 | await jobImplementation.FailedEvent((dynamic) jobMetadata.JobParam, ex); 121 | } 122 | catch (Exception failedEventException) 123 | { 124 | _settings.Logger.Error($"{jobMetadata.JobType}'s .FailedEvent threw", failedEventException); 125 | } 126 | } 127 | 128 | await HandleFallbackStrategy(jobMetadata); 129 | 130 | await _jobRepository.FailedJob(jobMetadata.JobId, ex); 131 | _settings.Logger.Debug("jobMetadata saved failed"); 132 | } 133 | else 134 | { 135 | await _jobRepository.RepeatJob(jobMetadata.JobId, GetNextStartFailedJobTime(jobMetadata), ex); 136 | _settings.Logger.Debug("jobMetadata saved repeat"); 137 | } 138 | } 139 | 140 | private DateTime GetNextStartFailedJobTime(JobMetadata jobMetadata) 141 | { 142 | IFailedRepeatStrategy strategy; 143 | 144 | if (jobMetadata.RepeatStrategy != null) 145 | { 146 | strategy = (IFailedRepeatStrategy) Activator.CreateInstance(jobMetadata.RepeatStrategy); 147 | } 148 | else 149 | { 150 | strategy = _settings.FailedRepeatStrategy; 151 | } 152 | 153 | return DateTime.UtcNow + strategy.GetNextStartInterval(jobMetadata.CountStarted); 154 | } 155 | 156 | private async Task ScheduleNextRecurrentIfPossible(JobMetadata metadata, Exception error) 157 | { 158 | var newStartAt = Utils.ParseAndGetNextOccurrence(metadata.Cron); 159 | 160 | if (newStartAt.HasValue) 161 | { 162 | await _jobRepository.RescheduleRecurrentJob(metadata.JobId, 163 | newStartAt.Value, error); 164 | } 165 | else 166 | { 167 | await _jobRepository.RemoveJob(metadata.JobId); 168 | } 169 | } 170 | 171 | private async Task ScheduleNextJobIfExists(JobMetadata metadata) 172 | { 173 | if (metadata.NextJob == null) 174 | { 175 | return; 176 | } 177 | 178 | await ScheduleJob(metadata.NextJob); 179 | _settings.Logger.Debug("next jobMetadata added"); 180 | } 181 | 182 | private async Task ScheduleFallbackJobIfExists(JobMetadata metadata) 183 | { 184 | if (metadata.FallbackJob == null) 185 | { 186 | return; 187 | } 188 | 189 | await ScheduleJob(metadata.FallbackJob); 190 | _settings.Logger.Debug("fallback jobMetadata added"); 191 | } 192 | 193 | private async Task ScheduleJob(JobMetadata metadata) 194 | { 195 | metadata.StartAt = DateTime.UtcNow + metadata.Delay.GetValueOrDefault(); 196 | 197 | await _jobRepository.AddJob(JobDb.CreatedJobDb(metadata, _settings.JsonSerializerSettings)); 198 | } 199 | 200 | private Task HandleFallbackStrategy(JobMetadata metadata) 201 | { 202 | switch (metadata.FallbackStrategyType) 203 | { 204 | case FallbackStrategyTypeEnum.GoToNextJob: 205 | return ScheduleNextJobIfExists(metadata); 206 | case FallbackStrategyTypeEnum.ScheduleFallbackJob: 207 | return ScheduleFallbackJobIfExists(metadata); 208 | default: 209 | return Task.CompletedTask; 210 | } 211 | } 212 | } 213 | } -------------------------------------------------------------------------------- /src/Horarium/Handlers/RecurrentJobSettingsAdder.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Threading.Tasks; 4 | using Horarium.Interfaces; 5 | using Horarium.Repository; 6 | 7 | namespace Horarium.Handlers 8 | { 9 | public class RecurrentJobSettingsAdder : IRecurrentJobSettingsAdder 10 | { 11 | private readonly IJobRepository _jobRepository; 12 | private readonly JsonSerializerSettings _jsonSerializerSettings; 13 | 14 | public RecurrentJobSettingsAdder(IJobRepository jobRepository, JsonSerializerSettings jsonSerializerSettings) 15 | { 16 | _jobRepository = jobRepository; 17 | _jsonSerializerSettings = jsonSerializerSettings; 18 | } 19 | 20 | public async Task Add(string cron, Type jobType, string jobKey) 21 | { 22 | var settings = new RecurrentJobSettingsMetadata(jobKey, jobType, cron); 23 | 24 | await _jobRepository.AddRecurrentJobSettings(RecurrentJobSettings.CreatedRecurrentJobSettings(settings)); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Horarium/Handlers/RunnerJobs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Horarium.Interfaces; 5 | using Horarium.Repository; 6 | using Newtonsoft.Json; 7 | 8 | namespace Horarium.Handlers 9 | { 10 | public class RunnerJobs : IRunnerJobs 11 | { 12 | private readonly string _machineName = Environment.MachineName + "_" + Guid.NewGuid(); 13 | private readonly IJobRepository _jobRepository; 14 | private readonly HorariumSettings _settings; 15 | private readonly JsonSerializerSettings _jsonSerializerSettings; 16 | private readonly IHorariumLogger _horariumLogger; 17 | private readonly IExecutorJob _executorJob; 18 | private Task _runnerTask; 19 | private readonly IUncompletedTaskList _uncompletedTaskList; 20 | 21 | private readonly TimeSpan _defaultJobThrottleInterval = TimeSpan.FromMilliseconds(100); 22 | 23 | private CancellationToken _cancellationToken; 24 | private readonly CancellationTokenSource _cancelTokenSource = new CancellationTokenSource(); 25 | 26 | public RunnerJobs(IJobRepository jobRepository, 27 | HorariumSettings settings, 28 | JsonSerializerSettings jsonSerializerSettings, 29 | IHorariumLogger horariumLogger, IExecutorJob executorJob, 30 | IUncompletedTaskList uncompletedTaskList) 31 | { 32 | _jobRepository = jobRepository; 33 | _settings = settings; 34 | _jsonSerializerSettings = jsonSerializerSettings; 35 | _horariumLogger = horariumLogger; 36 | _executorJob = executorJob; 37 | _uncompletedTaskList = uncompletedTaskList; 38 | } 39 | 40 | public void Start() 41 | { 42 | _horariumLogger.Debug("Starting RunnerJob..."); 43 | 44 | _cancellationToken = _cancelTokenSource.Token; 45 | _runnerTask = Task.Run(StartRunner, _cancellationToken); 46 | 47 | _horariumLogger.Debug("Started RunnerJob..."); 48 | } 49 | 50 | public async Task Stop(CancellationToken stopCancellationToken) 51 | { 52 | _cancelTokenSource.Cancel(false); 53 | 54 | try 55 | { 56 | await _runnerTask; 57 | } 58 | catch (TaskCanceledException) 59 | { 60 | //watcher был остановлен 61 | } 62 | 63 | await _uncompletedTaskList.WhenAllCompleted(stopCancellationToken); 64 | 65 | _horariumLogger.Debug("Stopped DeleterJob"); 66 | } 67 | 68 | private async Task StartRunner() 69 | { 70 | try 71 | { 72 | await StartRunnerInternal(_cancellationToken); 73 | } 74 | catch (TaskCanceledException) 75 | { 76 | throw; 77 | } 78 | catch (Exception ex) 79 | { 80 | _horariumLogger.Error("Остановлен StartPeriodicCheckStates", ex); 81 | _runnerTask = StartRunner(); 82 | throw; 83 | } 84 | } 85 | 86 | private async Task GetReadyJob() 87 | { 88 | JobDb job = null; 89 | 90 | try 91 | { 92 | job = await _jobRepository.GetReadyJob(_machineName, _settings.ObsoleteExecutingJob); 93 | } 94 | catch (Exception ex) 95 | { 96 | _horariumLogger.Error("Ошибка получения джоба из базы", ex); 97 | } 98 | 99 | if (job == null) return null; 100 | 101 | try 102 | { 103 | return job.ToJob(_jsonSerializerSettings); 104 | } 105 | catch (Exception ex) 106 | { 107 | _horariumLogger.Error("Ошибка парсинга метаданных джоба", ex); 108 | 109 | await _jobRepository.FailedJob(job.JobId, ex); 110 | } 111 | 112 | return null; 113 | } 114 | 115 | private async Task StartRunnerInternal(CancellationToken cancellationToken) 116 | { 117 | var jobWaitTime = _settings.IntervalStartJob; 118 | 119 | while (true) 120 | { 121 | var isJobRan = await TryRunJob(cancellationToken, jobWaitTime); 122 | if (!_settings.JobThrottleSettings.UseJobThrottle) 123 | { 124 | jobWaitTime = _settings.IntervalStartJob; 125 | continue; 126 | } 127 | 128 | jobWaitTime = !isJobRan ? GetNextIntervalStartJob(jobWaitTime) : _settings.IntervalStartJob; 129 | } 130 | } 131 | 132 | private async Task TryRunJob(CancellationToken cancellationToken, TimeSpan waitTime) 133 | { 134 | for (var i = 0; i < _settings.JobThrottleSettings.JobRetrievalAttempts; i++) 135 | { 136 | var job = await GetReadyJob(); 137 | var isJobReady = job != null; 138 | 139 | if (isJobReady) 140 | { 141 | _horariumLogger.Debug("Try to Run jobMetadata..."); 142 | 143 | var jobTask = Task.Run(() => _executorJob.Execute(job), CancellationToken.None); 144 | _uncompletedTaskList.Add(jobTask); 145 | } 146 | 147 | if (cancellationToken.IsCancellationRequested) 148 | { 149 | throw new TaskCanceledException(); 150 | } 151 | 152 | if (!waitTime.Equals(TimeSpan.Zero)) 153 | { 154 | await Task.Delay(waitTime, cancellationToken); 155 | } 156 | 157 | if (isJobReady) 158 | { 159 | return true; 160 | } 161 | } 162 | 163 | return false; 164 | } 165 | 166 | private TimeSpan GetNextIntervalStartJob(TimeSpan currentInterval) 167 | { 168 | if (currentInterval.Equals(TimeSpan.Zero)) 169 | { 170 | return _defaultJobThrottleInterval; 171 | } 172 | 173 | var nextInterval = 174 | currentInterval + 175 | TimeSpan.FromTicks((long) (currentInterval.Ticks * _settings.JobThrottleSettings.IntervalMultiplier)); 176 | 177 | var maxInterval = _settings.JobThrottleSettings.MaxJobThrottleInterval; 178 | 179 | return nextInterval > maxInterval ? maxInterval : nextInterval; 180 | } 181 | } 182 | } -------------------------------------------------------------------------------- /src/Horarium/Handlers/StatisticsJobs.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using Horarium.Interfaces; 4 | using Horarium.Repository; 5 | 6 | namespace Horarium.Handlers 7 | { 8 | public class StatisticsJobs : IStatisticsJobs 9 | { 10 | private readonly IJobRepository _jobRepository; 11 | 12 | public StatisticsJobs(IJobRepository jobRepository) 13 | { 14 | _jobRepository = jobRepository; 15 | } 16 | 17 | public Task> GetJobStatistic() 18 | { 19 | return _jobRepository.GetJobStatistic(); 20 | } 21 | 22 | } 23 | } -------------------------------------------------------------------------------- /src/Horarium/Handlers/UncompletedTaskList.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Horarium.Interfaces; 7 | 8 | namespace Horarium.Handlers 9 | { 10 | public class UncompletedTaskList : IUncompletedTaskList 11 | { 12 | private readonly LinkedList _uncompletedTasks = new LinkedList(); 13 | private readonly object _lockObject = new object(); 14 | 15 | public int Count 16 | { 17 | get 18 | { 19 | lock (_lockObject) return _uncompletedTasks.Count; 20 | } 21 | } 22 | 23 | public void Add(Task task) 24 | { 25 | LinkedListNode linkedListNode; 26 | 27 | lock (_lockObject) 28 | { 29 | linkedListNode = _uncompletedTasks.AddLast(task); 30 | } 31 | 32 | task.ContinueWith((t, state) => 33 | { 34 | lock (_lockObject) 35 | { 36 | _uncompletedTasks.Remove((LinkedListNode) state); 37 | } 38 | }, linkedListNode, CancellationToken.None); 39 | } 40 | 41 | public async Task WhenAllCompleted(CancellationToken cancellationToken) 42 | { 43 | Task[] tasksToAwait; 44 | lock (_lockObject) 45 | { 46 | tasksToAwait = _uncompletedTasks 47 | // get rid of fault state, Task.WhenAll shall not throw 48 | .Select(x => x.ContinueWith((t) => { }, CancellationToken.None)) 49 | .ToArray(); 50 | } 51 | 52 | var whenAbandon = Task.Delay(Timeout.Infinite, cancellationToken); 53 | var whenAllCompleted = Task.WhenAll(tasksToAwait); 54 | 55 | await Task.WhenAny(whenAbandon, whenAllCompleted); 56 | 57 | if (cancellationToken.IsCancellationRequested) 58 | throw new OperationCanceledException( 59 | "Horarium stop timeout is expired. One or many jobs are still running. These jobs may not save their state.", 60 | cancellationToken); 61 | 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /src/Horarium/Horarium.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0 4 | Horarium 5 | 1.0.0$(VersionSuffix) 6 | Horarium 7 | bobreshovr 8 | Horarium is the .Net library to manage background jobs 9 | https://github.com/TinkoffCreditSystems/Horarium 10 | https://github.com/TinkoffCreditSystems/Horarium 11 | Apache-2.0 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Horarium/HorariumClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Horarium.Builders.JobSequenceBuilder; 5 | using Horarium.Builders.Recurrent; 6 | using Horarium.Handlers; 7 | using Horarium.Interfaces; 8 | using Horarium.Repository; 9 | using Horarium.Builders.Parameterized; 10 | 11 | namespace Horarium 12 | { 13 | public class HorariumClient : IHorarium 14 | { 15 | private readonly HorariumSettings _settings; 16 | private readonly IAdderJobs _adderJobs; 17 | private readonly IStatisticsJobs _statisticsJobs; 18 | 19 | public HorariumClient(IJobRepository jobRepository):this(jobRepository, new HorariumSettings()) 20 | { 21 | } 22 | 23 | public HorariumClient(IJobRepository jobRepository, HorariumSettings settings) 24 | { 25 | _settings = settings; 26 | _adderJobs = new AdderJobs(jobRepository, settings.JsonSerializerSettings); 27 | _statisticsJobs = new StatisticsJobs(jobRepository); 28 | } 29 | 30 | private TimeSpan GlobalObsoleteInterval => _settings.ObsoleteExecutingJob; 31 | 32 | public IRecurrentJobBuilder CreateRecurrent(string cron) where TJob : IJobRecurrent 33 | { 34 | return new RecurrentJobBuilder(_adderJobs, cron, typeof(TJob), GlobalObsoleteInterval); 35 | } 36 | 37 | public async Task Schedule(TJobParam param, Action configure = null) where TJob : IJob 38 | { 39 | var jobBuilder = new JobSequenceBuilder(param, _settings.ObsoleteExecutingJob); 40 | 41 | configure?.Invoke(jobBuilder); 42 | 43 | var job = jobBuilder.Build(); 44 | 45 | await _adderJobs.AddEnqueueJob(job); 46 | } 47 | 48 | [Obsolete("use Schedule method instead")] 49 | public IParameterizedJobBuilder Create(TJobParam param) where TJob : IJob 50 | { 51 | return new ParameterizedJobBuilder(_adderJobs, param, _settings.ObsoleteExecutingJob); 52 | } 53 | 54 | public Task> GetJobStatistic() 55 | { 56 | return _statisticsJobs.GetJobStatistic(); 57 | } 58 | 59 | public void Dispose() 60 | { 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /src/Horarium/HorariumServer.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Horarium.Handlers; 5 | using Horarium.Interfaces; 6 | using Horarium.Repository; 7 | 8 | [assembly: InternalsVisibleTo("Horarium.Test")] 9 | 10 | namespace Horarium 11 | { 12 | public class HorariumServer : HorariumClient, IHorarium 13 | { 14 | private readonly HorariumSettings _settings; 15 | private IRunnerJobs _runnerJobs; 16 | 17 | private readonly IJobRepository _jobRepository; 18 | 19 | public HorariumServer(IJobRepository jobRepository) 20 | : this(jobRepository, new HorariumSettings()) 21 | { 22 | } 23 | 24 | public HorariumServer(IJobRepository jobRepository, HorariumSettings settings) 25 | : base(jobRepository, settings) 26 | { 27 | _settings = settings; 28 | _jobRepository = jobRepository; 29 | } 30 | 31 | public void Start() 32 | { 33 | var executorJob = new ExecutorJob(_jobRepository, _settings); 34 | 35 | _runnerJobs = new RunnerJobs(_jobRepository, _settings, _settings.JsonSerializerSettings, _settings.Logger, 36 | executorJob, new UncompletedTaskList()); 37 | _runnerJobs.Start(); 38 | } 39 | 40 | public Task Stop(CancellationToken stopCancellationToken) 41 | { 42 | return _runnerJobs.Stop(stopCancellationToken); 43 | } 44 | 45 | public new void Dispose() 46 | { 47 | Stop(CancellationToken.None); 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /src/Horarium/HorariumSettings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Horarium.Interfaces; 3 | using Newtonsoft.Json; 4 | 5 | namespace Horarium 6 | { 7 | public class HorariumSettings 8 | { 9 | public TimeSpan IntervalStartJob { get; set; } = TimeSpan.FromMilliseconds(100); 10 | 11 | public TimeSpan ObsoleteExecutingJob { get; set; } = TimeSpan.FromMinutes(5); 12 | 13 | public JobThrottleSettings JobThrottleSettings { get; set; } = new JobThrottleSettings(); 14 | 15 | public IJobScopeFactory JobScopeFactory { get; set; } = new DefaultJobScopeFactory(); 16 | 17 | public IHorariumLogger Logger { get; set; } = new EmptyLogger(); 18 | 19 | public JsonSerializerSettings JsonSerializerSettings { get; set; } = new JsonSerializerSettings(); 20 | 21 | public IFailedRepeatStrategy FailedRepeatStrategy { get; set; } = new DefaultRepeatStrategy(); 22 | 23 | public int MaxRepeatCount { get; set; } = 10; 24 | } 25 | } -------------------------------------------------------------------------------- /src/Horarium/Interfaces/IAdderJobs.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Horarium.Interfaces 4 | { 5 | public interface IAdderJobs 6 | { 7 | Task AddEnqueueJob(JobMetadata jobMetadata); 8 | 9 | Task AddRecurrentJob(JobMetadata jobMetadata); 10 | } 11 | } -------------------------------------------------------------------------------- /src/Horarium/Interfaces/IAllRepeatesIsFailed.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace Horarium.Interfaces 5 | { 6 | public interface IAllRepeatesIsFailed 7 | { 8 | Task FailedEvent(object param, Exception ex); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Horarium/Interfaces/IExecutorJob.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Horarium.Interfaces 4 | { 5 | public interface IExecutorJob 6 | { 7 | Task Execute(JobMetadata jobMetadata); 8 | } 9 | } -------------------------------------------------------------------------------- /src/Horarium/Interfaces/IFailedRepeatStrategy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Horarium.Interfaces 4 | { 5 | public interface IFailedRepeatStrategy 6 | { 7 | TimeSpan GetNextStartInterval(int countStarted); 8 | } 9 | } -------------------------------------------------------------------------------- /src/Horarium/Interfaces/IHorarium.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Horarium.Builders.JobSequenceBuilder; 5 | using Horarium.Builders.Recurrent; 6 | using Horarium.Builders.Parameterized; 7 | 8 | namespace Horarium.Interfaces 9 | { 10 | public interface IHorarium : IDisposable 11 | { 12 | /// 13 | /// Return count of jobs in status 14 | /// 15 | /// 16 | Task> GetJobStatistic(); 17 | 18 | /// 19 | /// Create one time job 20 | /// 21 | /// Type of job, job will create from factory 22 | /// Type of parameters 23 | /// 24 | [Obsolete("use Schedule method instead")] 25 | IParameterizedJobBuilder Create(TJobParam param) where TJob : IJob; 26 | 27 | /// 28 | /// Create builder for recurrent job with cron 29 | /// 30 | /// Cron 31 | /// Type of job, job will create from factory 32 | /// 33 | IRecurrentJobBuilder CreateRecurrent(string cron) where TJob : IJobRecurrent; 34 | 35 | /// 36 | /// Create one time job 37 | /// 38 | /// Type of job, job will create from factory 39 | /// Type of parameters 40 | /// 41 | Task Schedule(TJobParam param, Action configure = null) where TJob : IJob; 42 | } 43 | } -------------------------------------------------------------------------------- /src/Horarium/Interfaces/IHorariumLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Horarium.Interfaces 4 | { 5 | public interface IHorariumLogger 6 | { 7 | void Debug(string msg); 8 | 9 | void Debug(Exception ex); 10 | 11 | void Error(Exception ex); 12 | 13 | void Error(string message, Exception ex); 14 | } 15 | } -------------------------------------------------------------------------------- /src/Horarium/Interfaces/IJob.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Horarium.Interfaces 4 | { 5 | public interface IJob 6 | { 7 | Task Execute(TJobParam param); 8 | } 9 | 10 | public interface IJobRecurrent 11 | { 12 | Task Execute(); 13 | } 14 | } -------------------------------------------------------------------------------- /src/Horarium/Interfaces/IJobScope.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Horarium.Interfaces 4 | { 5 | public interface IJobScope : IDisposable 6 | { 7 | object CreateJob(Type type); 8 | } 9 | } -------------------------------------------------------------------------------- /src/Horarium/Interfaces/IJobScopeFactory.cs: -------------------------------------------------------------------------------- 1 | namespace Horarium.Interfaces 2 | { 3 | public interface IJobScopeFactory 4 | { 5 | IJobScope Create(); 6 | } 7 | } -------------------------------------------------------------------------------- /src/Horarium/Interfaces/IRecurrentJobSettingsAdder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace Horarium.Interfaces 5 | { 6 | public interface IRecurrentJobSettingsAdder 7 | { 8 | Task Add(string cron, Type jobType, string jobKey); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Horarium/Interfaces/IRunnerJobs.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | 4 | namespace Horarium.Interfaces 5 | { 6 | public interface IRunnerJobs 7 | { 8 | void Start(); 9 | 10 | /// 11 | /// Stops scheduling next jobs and awaits currently running jobs. 12 | /// If is cancelled, then abandons running jobs. 13 | /// 14 | Task Stop(CancellationToken stopCancellationToken); 15 | 16 | } 17 | } -------------------------------------------------------------------------------- /src/Horarium/Interfaces/ISequenceJobs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace Horarium.Interfaces 5 | { 6 | public interface ISequenceJobs 7 | { 8 | /// 9 | /// Indicate the next job for sequence execution 10 | /// 11 | /// The job parameters, will be send at start 12 | /// Job type, the job will be created through the factory 13 | /// Parameters type 14 | /// 15 | ISequenceJobs NextJob(TJobParam param) where TJob : IJob; 16 | 17 | /// 18 | /// Indicate the next job for sequence with delay execution 19 | /// 20 | /// The job parameters, will be send at start 21 | /// Job execution delay, after the previous execution 22 | /// Job type, the job will be created through the factory 23 | /// Parameters type 24 | /// 25 | ISequenceJobs NextJob(TJobParam param, TimeSpan delay) where TJob : IJob; 26 | 27 | /// 28 | /// Run this sequence 29 | /// 30 | /// 31 | Task Run(); 32 | } 33 | } -------------------------------------------------------------------------------- /src/Horarium/Interfaces/IStatisticsJobs.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | 4 | namespace Horarium.Interfaces 5 | { 6 | public interface IStatisticsJobs 7 | { 8 | Task> GetJobStatistic(); 9 | } 10 | } -------------------------------------------------------------------------------- /src/Horarium/Interfaces/IUncompletedTaskList.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace Horarium.Interfaces 6 | { 7 | /// 8 | /// Keeps references to a task until it is completed. 9 | /// 10 | public interface IUncompletedTaskList 11 | { 12 | /// 13 | /// Adds new task to monitor. 14 | /// 15 | void Add(Task task); 16 | 17 | /// 18 | /// Returns task that will complete (with success) when all currently running tasks complete or fail. 19 | /// 20 | /// If cancelled, throws immediately. 21 | Task WhenAllCompleted(CancellationToken cancellationToken); 22 | } 23 | } -------------------------------------------------------------------------------- /src/Horarium/JobMetadata.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Horarium.Fallbacks; 3 | 4 | namespace Horarium 5 | { 6 | public class JobMetadata 7 | { 8 | public string JobId { get; set; } 9 | 10 | public Type JobType { get; set; } 11 | 12 | public object JobParam { get; set; } 13 | 14 | public JobStatus Status { get; set; } 15 | 16 | public int CountStarted { get; set; } 17 | 18 | public string ExecutedMachine { get; set; } 19 | 20 | public DateTime StartedExecuting { get; set; } 21 | 22 | public DateTime StartAt { get; set; } 23 | 24 | public JobMetadata NextJob { get; set; } 25 | 26 | public string JobKey { get; set; } 27 | 28 | public string Cron { get; set; } 29 | 30 | public TimeSpan? Delay { get; set; } 31 | 32 | public TimeSpan ObsoleteInterval { get; set; } 33 | 34 | public Type RepeatStrategy { get; set; } 35 | 36 | public int MaxRepeatCount { get; set; } 37 | 38 | public FallbackStrategyTypeEnum? FallbackStrategyType { get; set; } 39 | 40 | public JobMetadata FallbackJob { get; set; } 41 | } 42 | } -------------------------------------------------------------------------------- /src/Horarium/JobStatus.cs: -------------------------------------------------------------------------------- 1 | namespace Horarium 2 | { 3 | public enum JobStatus 4 | { 5 | Ready = 0, 6 | Executing = 1, 7 | Failed = 2, 8 | RepeatJob = 4 9 | } 10 | } -------------------------------------------------------------------------------- /src/Horarium/JobThrottleSettings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Horarium 4 | { 5 | public class JobThrottleSettings 6 | { 7 | /// 8 | /// When `true`, IntervalStartJob will automatically increase if there is no jobs available 9 | /// 10 | public bool UseJobThrottle { get; set; } 11 | 12 | /// 13 | /// After all attempts are exhausted, waiting interval is increased by formula: 14 | /// currentInterval + (currentInterval * intervalMultiplier) 15 | /// 16 | public int JobRetrievalAttempts { get; set; } = 10; 17 | 18 | /// 19 | /// Multiplier to get the next waiting interval 20 | /// 21 | public double IntervalMultiplier { get; set; } = 0.25; 22 | 23 | /// 24 | /// Maximum waiting interval 25 | /// 26 | public TimeSpan MaxJobThrottleInterval { get; set; } = TimeSpan.FromSeconds(30); 27 | } 28 | } -------------------------------------------------------------------------------- /src/Horarium/RecurrentJobSettingsMetadata.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Horarium 4 | { 5 | public class RecurrentJobSettingsMetadata 6 | { 7 | public RecurrentJobSettingsMetadata(string jobKey, Type jobType, string cron) 8 | { 9 | JobKey = jobKey; 10 | JobType = jobType; 11 | Cron = cron; 12 | } 13 | 14 | public string JobKey { get; private set; } 15 | 16 | public Type JobType { get; private set; } 17 | 18 | public string Cron { get; private set; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Horarium/Repository/IJobRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | 5 | namespace Horarium.Repository 6 | { 7 | public interface IJobRepository 8 | { 9 | Task GetReadyJob(string machineName, TimeSpan obsoleteTime); 10 | 11 | Task AddJob(JobDb job); 12 | 13 | Task FailedJob(string jobId, Exception error); 14 | 15 | Task RemoveJob(string jobId); 16 | 17 | Task RepeatJob(string jobId, DateTime startAt, Exception error); 18 | 19 | Task AddRecurrentJob(JobDb job); 20 | 21 | Task AddRecurrentJobSettings(RecurrentJobSettings settings); 22 | 23 | Task> GetJobStatistic(); 24 | 25 | Task RescheduleRecurrentJob(string jobId, DateTime startAt, Exception error); 26 | } 27 | } -------------------------------------------------------------------------------- /src/Horarium/Repository/JobDb.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Horarium.Fallbacks; 3 | using Newtonsoft.Json; 4 | 5 | namespace Horarium.Repository 6 | { 7 | public class JobDb 8 | { 9 | public static JobDb CreatedJobDb(JobMetadata jobMetadata, JsonSerializerSettings jsonSerializerSettings) 10 | { 11 | return new JobDb 12 | { 13 | JobKey = jobMetadata.JobKey, 14 | JobId = jobMetadata.JobId, 15 | Status = jobMetadata.Status, 16 | JobType = jobMetadata.JobType.AssemblyQualifiedNameWithoutVersion(), 17 | JobParamType = jobMetadata.JobParam?.GetType().AssemblyQualifiedNameWithoutVersion(), 18 | JobParam = jobMetadata.JobParam?.ToJson(jobMetadata.JobParam.GetType(), jsonSerializerSettings), 19 | CountStarted = jobMetadata.CountStarted, 20 | StartedExecuting = jobMetadata.StartedExecuting, 21 | ExecutedMachine = jobMetadata.ExecutedMachine, 22 | StartAt = jobMetadata.StartAt, 23 | NextJob = 24 | jobMetadata.NextJob != null ? CreatedJobDb(jobMetadata.NextJob, jsonSerializerSettings) : null, 25 | Cron = jobMetadata.Cron, 26 | Delay = jobMetadata.Delay, 27 | ObsoleteInterval = jobMetadata.ObsoleteInterval, 28 | RepeatStrategy = jobMetadata.RepeatStrategy?.AssemblyQualifiedNameWithoutVersion(), 29 | MaxRepeatCount = jobMetadata.MaxRepeatCount, 30 | FallbackStrategyType = jobMetadata.FallbackStrategyType, 31 | FallbackJob = jobMetadata.FallbackJob != null ? CreatedJobDb(jobMetadata.FallbackJob, jsonSerializerSettings) : null, 32 | }; 33 | } 34 | 35 | public string JobId { get; set; } 36 | 37 | public string JobKey { get; set; } 38 | 39 | public string JobType { get; set; } 40 | 41 | public string JobParamType { get; set; } 42 | 43 | public string JobParam { get; set; } 44 | 45 | public JobStatus Status { get; set; } 46 | 47 | public int CountStarted { get; set; } 48 | 49 | public string ExecutedMachine { get; set; } 50 | 51 | public DateTime StartedExecuting { get; set; } 52 | 53 | public DateTime StartAt { get; set; } 54 | 55 | public JobDb NextJob { get; set; } 56 | 57 | public string Error { get; set; } 58 | 59 | public string Cron { get; set; } 60 | 61 | public TimeSpan? Delay { get; set; } 62 | 63 | public TimeSpan ObsoleteInterval { get; set; } 64 | 65 | public string RepeatStrategy { get; set; } 66 | 67 | public int MaxRepeatCount { get; set; } 68 | 69 | public FallbackStrategyTypeEnum? FallbackStrategyType { get; set; } 70 | 71 | public JobDb FallbackJob { get; set; } 72 | 73 | public JobMetadata ToJob(JsonSerializerSettings jsonSerializerSettings) 74 | { 75 | return new JobMetadata 76 | { 77 | JobId = JobId, 78 | JobKey = JobKey, 79 | Status = Status, 80 | CountStarted = CountStarted, 81 | StartedExecuting = StartedExecuting, 82 | ExecutedMachine = ExecutedMachine, 83 | JobType = Type.GetType(JobType, true), 84 | JobParam = JobParam?.FromJson(Type.GetType(JobParamType), jsonSerializerSettings), 85 | StartAt = StartAt, 86 | NextJob = NextJob?.ToJob(jsonSerializerSettings), 87 | Cron = Cron, 88 | Delay = Delay, 89 | ObsoleteInterval = ObsoleteInterval, 90 | RepeatStrategy = string.IsNullOrEmpty(RepeatStrategy) ? null : Type.GetType(RepeatStrategy, true), 91 | MaxRepeatCount = MaxRepeatCount, 92 | FallbackStrategyType = FallbackStrategyType, 93 | FallbackJob = FallbackJob?.ToJob(jsonSerializerSettings) 94 | }; 95 | } 96 | } 97 | } -------------------------------------------------------------------------------- /src/Horarium/Repository/RecurrentJobSettings.cs: -------------------------------------------------------------------------------- 1 | namespace Horarium.Repository 2 | { 3 | public class RecurrentJobSettings 4 | { 5 | public static RecurrentJobSettings CreatedRecurrentJobSettings(RecurrentJobSettingsMetadata jobMetadata) 6 | { 7 | return new RecurrentJobSettings 8 | { 9 | JobKey = jobMetadata.JobKey, 10 | JobType = jobMetadata.JobType.AssemblyQualifiedNameWithoutVersion(), 11 | Cron = jobMetadata.Cron 12 | }; 13 | } 14 | 15 | public string JobKey { get; private set; } 16 | 17 | public string JobType { get; private set; } 18 | 19 | public string Cron { get; private set; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Horarium/Utils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using Cronos; 4 | using Newtonsoft.Json; 5 | 6 | namespace Horarium 7 | { 8 | internal static class Utils 9 | { 10 | public static string ToJson(this object obj, Type type, JsonSerializerSettings jsonSerializerSettings) 11 | { 12 | return JsonConvert.SerializeObject(obj, type, jsonSerializerSettings); 13 | } 14 | 15 | public static object FromJson(this string json, Type type, JsonSerializerSettings jsonSerializerSettings) 16 | { 17 | return JsonConvert.DeserializeObject(json, type, jsonSerializerSettings); 18 | } 19 | 20 | public static string AssemblyQualifiedNameWithoutVersion(this Type type) 21 | { 22 | string retValue = type.FullName + ", " + type.GetTypeInfo().Assembly.GetName().Name; 23 | return retValue; 24 | } 25 | 26 | public static DateTime? ParseAndGetNextOccurrence(string cron) 27 | { 28 | var expression = CronExpression.Parse(cron, CronFormat.IncludeSeconds); 29 | 30 | return expression.GetNextOccurrence(DateTime.UtcNow, TimeZoneInfo.Local); 31 | } 32 | } 33 | } --------------------------------------------------------------------------------