diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c4efe2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,261 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +#*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..418de4c --- /dev/null +++ b/LICENSE @@ -0,0 +1,174 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..ab62036 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# Sample MassTransit Quartz Scheduler + +This sample contains three examples: + +1. Net461 using TopShelf +2. NetCore Win Svc (for still deploying on a Windows Server) +3. NetCore (linux, windows, mac...) + +The persistence mechanism used in this example is SQLServer, however Quartz.net supports [all of these](https://github.com/quartznet/quartznet/tree/master/database/tables). + +## Building + +Connect to your development SQLServer, windows users is most likely (localdb)\MSSQLLocalDb, and [run this script](create_quartz_tables.sql) + +Open the .sln, and run any one of the three projects. Done, you have a MT Scheduler! \ No newline at end of file diff --git a/create_quartz_tables.sql b/create_quartz_tables.sql new file mode 100644 index 0000000..06a368e --- /dev/null +++ b/create_quartz_tables.sql @@ -0,0 +1,367 @@ +-- this script is for SQL Server and Azure SQL + +GO + +IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[FK_QRTZ_TRIGGERS_QRTZ_JOB_DETAILS]') AND OBJECTPROPERTY(id, N'ISFOREIGNKEY') = 1) +ALTER TABLE [dbo].[QRTZ_TRIGGERS] DROP CONSTRAINT FK_QRTZ_TRIGGERS_QRTZ_JOB_DETAILS +GO + +IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[FK_QRTZ_CRON_TRIGGERS_QRTZ_TRIGGERS]') AND OBJECTPROPERTY(id, N'ISFOREIGNKEY') = 1) +ALTER TABLE [dbo].[QRTZ_CRON_TRIGGERS] DROP CONSTRAINT FK_QRTZ_CRON_TRIGGERS_QRTZ_TRIGGERS +GO + +IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[FK_QRTZ_SIMPLE_TRIGGERS_QRTZ_TRIGGERS]') AND OBJECTPROPERTY(id, N'ISFOREIGNKEY') = 1) +ALTER TABLE [dbo].[QRTZ_SIMPLE_TRIGGERS] DROP CONSTRAINT FK_QRTZ_SIMPLE_TRIGGERS_QRTZ_TRIGGERS +GO + +IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[FK_QRTZ_SIMPROP_TRIGGERS_QRTZ_TRIGGERS]') AND OBJECTPROPERTY(id, N'ISFOREIGNKEY') = 1) +ALTER TABLE [dbo].[QRTZ_SIMPROP_TRIGGERS] DROP CONSTRAINT FK_QRTZ_SIMPROP_TRIGGERS_QRTZ_TRIGGERS +GO + +IF EXISTS (SELECT * FROM sys.foreign_keys WHERE object_id = OBJECT_ID(N'[dbo].[FK_QRTZ_JOB_LISTENERS_QRTZ_JOB_DETAILS]') AND parent_object_id = OBJECT_ID(N'[dbo].[QRTZ_JOB_LISTENERS]')) +ALTER TABLE [dbo].[QRTZ_JOB_LISTENERS] DROP CONSTRAINT [FK_QRTZ_JOB_LISTENERS_QRTZ_JOB_DETAILS] + +IF EXISTS (SELECT * FROM sys.foreign_keys WHERE object_id = OBJECT_ID(N'[dbo].[FK_QRTZ_TRIGGER_LISTENERS_QRTZ_TRIGGERS]') AND parent_object_id = OBJECT_ID(N'[dbo].[QRTZ_TRIGGER_LISTENERS]')) +ALTER TABLE [dbo].[QRTZ_TRIGGER_LISTENERS] DROP CONSTRAINT [FK_QRTZ_TRIGGER_LISTENERS_QRTZ_TRIGGERS] + + +IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[QRTZ_CALENDARS]') AND OBJECTPROPERTY(id, N'ISUSERTABLE') = 1) +DROP TABLE [dbo].[QRTZ_CALENDARS] +GO + +IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[QRTZ_CRON_TRIGGERS]') AND OBJECTPROPERTY(id, N'ISUSERTABLE') = 1) +DROP TABLE [dbo].[QRTZ_CRON_TRIGGERS] +GO + +IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[QRTZ_BLOB_TRIGGERS]') AND OBJECTPROPERTY(id, N'ISUSERTABLE') = 1) +DROP TABLE [dbo].[QRTZ_BLOB_TRIGGERS] +GO + +IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[QRTZ_FIRED_TRIGGERS]') AND OBJECTPROPERTY(id, N'ISUSERTABLE') = 1) +DROP TABLE [dbo].[QRTZ_FIRED_TRIGGERS] +GO + +IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[QRTZ_PAUSED_TRIGGER_GRPS]') AND OBJECTPROPERTY(id, N'ISUSERTABLE') = 1) +DROP TABLE [dbo].[QRTZ_PAUSED_TRIGGER_GRPS] +GO + +IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[QRTZ_JOB_LISTENERS]') AND type in (N'U')) +DROP TABLE [dbo].[QRTZ_JOB_LISTENERS] + +IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[QRTZ_SCHEDULER_STATE]') AND OBJECTPROPERTY(id, N'ISUSERTABLE') = 1) +DROP TABLE [dbo].[QRTZ_SCHEDULER_STATE] +GO + +IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[QRTZ_LOCKS]') AND OBJECTPROPERTY(id, N'ISUSERTABLE') = 1) +DROP TABLE [dbo].[QRTZ_LOCKS] +GO +IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[QRTZ_TRIGGER_LISTENERS]') AND type in (N'U')) +DROP TABLE [dbo].[QRTZ_TRIGGER_LISTENERS] + + +IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[QRTZ_JOB_DETAILS]') AND OBJECTPROPERTY(id, N'ISUSERTABLE') = 1) +DROP TABLE [dbo].[QRTZ_JOB_DETAILS] +GO + +IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[QRTZ_SIMPLE_TRIGGERS]') AND OBJECTPROPERTY(id, N'ISUSERTABLE') = 1) +DROP TABLE [dbo].[QRTZ_SIMPLE_TRIGGERS] +GO + +IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[QRTZ_SIMPROP_TRIGGERS]') AND OBJECTPROPERTY(id, N'ISUSERTABLE') = 1) +DROP TABLE [dbo].QRTZ_SIMPROP_TRIGGERS +GO + +IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[QRTZ_TRIGGERS]') AND OBJECTPROPERTY(id, N'ISUSERTABLE') = 1) +DROP TABLE [dbo].[QRTZ_TRIGGERS] +GO + +CREATE TABLE [dbo].[QRTZ_CALENDARS] ( + [SCHED_NAME] [NVARCHAR] (120) NOT NULL , + [CALENDAR_NAME] [NVARCHAR] (200) NOT NULL , + [CALENDAR] [VARBINARY](MAX) NOT NULL +) +GO + +CREATE TABLE [dbo].[QRTZ_CRON_TRIGGERS] ( + [SCHED_NAME] [NVARCHAR] (120) NOT NULL , + [TRIGGER_NAME] [NVARCHAR] (150) NOT NULL , + [TRIGGER_GROUP] [NVARCHAR] (150) NOT NULL , + [CRON_EXPRESSION] [NVARCHAR] (120) NOT NULL , + [TIME_ZONE_ID] [NVARCHAR] (80) +) +GO + +CREATE TABLE [dbo].[QRTZ_FIRED_TRIGGERS] ( + [SCHED_NAME] [NVARCHAR] (120) NOT NULL , + [ENTRY_ID] [NVARCHAR] (140) NOT NULL , + [TRIGGER_NAME] [NVARCHAR] (150) NOT NULL , + [TRIGGER_GROUP] [NVARCHAR] (150) NOT NULL , + [INSTANCE_NAME] [NVARCHAR] (200) NOT NULL , + [FIRED_TIME] [BIGINT] NOT NULL , + [SCHED_TIME] [BIGINT] NOT NULL , + [PRIORITY] [INTEGER] NOT NULL , + [STATE] [NVARCHAR] (16) NOT NULL, + [JOB_NAME] [NVARCHAR] (150) NULL , + [JOB_GROUP] [NVARCHAR] (150) NULL , + [IS_NONCONCURRENT] BIT NULL , + [REQUESTS_RECOVERY] BIT NULL +) +GO + +CREATE TABLE [dbo].[QRTZ_PAUSED_TRIGGER_GRPS] ( + [SCHED_NAME] [NVARCHAR] (120) NOT NULL , + [TRIGGER_GROUP] [NVARCHAR] (150) NOT NULL +) +GO + +CREATE TABLE [dbo].[QRTZ_SCHEDULER_STATE] ( + [SCHED_NAME] [NVARCHAR] (120) NOT NULL , + [INSTANCE_NAME] [NVARCHAR] (200) NOT NULL , + [LAST_CHECKIN_TIME] [BIGINT] NOT NULL , + [CHECKIN_INTERVAL] [BIGINT] NOT NULL +) +GO + +CREATE TABLE [dbo].[QRTZ_LOCKS] ( + [SCHED_NAME] [NVARCHAR] (120) NOT NULL , + [LOCK_NAME] [NVARCHAR] (40) NOT NULL +) +GO + +CREATE TABLE [dbo].[QRTZ_JOB_DETAILS] ( + [SCHED_NAME] [NVARCHAR] (120) NOT NULL , + [JOB_NAME] [NVARCHAR] (150) NOT NULL , + [JOB_GROUP] [NVARCHAR] (150) NOT NULL , + [DESCRIPTION] [NVARCHAR] (250) NULL , + [JOB_CLASS_NAME] [NVARCHAR] (250) NOT NULL , + [IS_DURABLE] BIT NOT NULL , + [IS_NONCONCURRENT] BIT NOT NULL , + [IS_UPDATE_DATA] BIT NOT NULL , + [REQUESTS_RECOVERY] BIT NOT NULL , + [JOB_DATA] [VARBINARY](MAX) NULL +) +GO + +CREATE TABLE [dbo].[QRTZ_SIMPLE_TRIGGERS] ( + [SCHED_NAME] [NVARCHAR] (120) NOT NULL , + [TRIGGER_NAME] [NVARCHAR] (150) NOT NULL , + [TRIGGER_GROUP] [NVARCHAR] (150) NOT NULL , + [REPEAT_COUNT] [INTEGER] NOT NULL , + [REPEAT_INTERVAL] [BIGINT] NOT NULL , + [TIMES_TRIGGERED] [INTEGER] NOT NULL +) +GO + +CREATE TABLE [dbo].[QRTZ_SIMPROP_TRIGGERS] ( + [SCHED_NAME] [NVARCHAR] (120) NOT NULL , + [TRIGGER_NAME] [NVARCHAR] (150) NOT NULL , + [TRIGGER_GROUP] [NVARCHAR] (150) NOT NULL , + [STR_PROP_1] [NVARCHAR] (512) NULL, + [STR_PROP_2] [NVARCHAR] (512) NULL, + [STR_PROP_3] [NVARCHAR] (512) NULL, + [INT_PROP_1] [INT] NULL, + [INT_PROP_2] [INT] NULL, + [LONG_PROP_1] [BIGINT] NULL, + [LONG_PROP_2] [BIGINT] NULL, + [DEC_PROP_1] [NUMERIC] (13,4) NULL, + [DEC_PROP_2] [NUMERIC] (13,4) NULL, + [BOOL_PROP_1] BIT NULL, + [BOOL_PROP_2] BIT NULL, + [TIME_ZONE_ID] [NVARCHAR] (80) NULL +) +GO + +CREATE TABLE [dbo].[QRTZ_BLOB_TRIGGERS] ( + [SCHED_NAME] [NVARCHAR] (120) NOT NULL , + [TRIGGER_NAME] [NVARCHAR] (150) NOT NULL , + [TRIGGER_GROUP] [NVARCHAR] (150) NOT NULL , + [BLOB_DATA] [VARBINARY](MAX) NULL +) +GO + +CREATE TABLE [dbo].[QRTZ_TRIGGERS] ( + [SCHED_NAME] [NVARCHAR] (120) NOT NULL , + [TRIGGER_NAME] [NVARCHAR] (150) NOT NULL , + [TRIGGER_GROUP] [NVARCHAR] (150) NOT NULL , + [JOB_NAME] [NVARCHAR] (150) NOT NULL , + [JOB_GROUP] [NVARCHAR] (150) NOT NULL , + [DESCRIPTION] [NVARCHAR] (250) NULL , + [NEXT_FIRE_TIME] [BIGINT] NULL , + [PREV_FIRE_TIME] [BIGINT] NULL , + [PRIORITY] [INTEGER] NULL , + [TRIGGER_STATE] [NVARCHAR] (16) NOT NULL , + [TRIGGER_TYPE] [NVARCHAR] (8) NOT NULL , + [START_TIME] [BIGINT] NOT NULL , + [END_TIME] [BIGINT] NULL , + [CALENDAR_NAME] [NVARCHAR] (200) NULL , + [MISFIRE_INSTR] [INTEGER] NULL , + [JOB_DATA] [VARBINARY](MAX) NULL +) +GO + +ALTER TABLE [dbo].[QRTZ_CALENDARS] WITH NOCHECK ADD + CONSTRAINT [PK_QRTZ_CALENDARS] PRIMARY KEY CLUSTERED + ( + [SCHED_NAME], + [CALENDAR_NAME] + ) +GO + +ALTER TABLE [dbo].[QRTZ_CRON_TRIGGERS] WITH NOCHECK ADD + CONSTRAINT [PK_QRTZ_CRON_TRIGGERS] PRIMARY KEY CLUSTERED + ( + [SCHED_NAME], + [TRIGGER_NAME], + [TRIGGER_GROUP] + ) +GO + +ALTER TABLE [dbo].[QRTZ_FIRED_TRIGGERS] WITH NOCHECK ADD + CONSTRAINT [PK_QRTZ_FIRED_TRIGGERS] PRIMARY KEY CLUSTERED + ( + [SCHED_NAME], + [ENTRY_ID] + ) +GO + +ALTER TABLE [dbo].[QRTZ_PAUSED_TRIGGER_GRPS] WITH NOCHECK ADD + CONSTRAINT [PK_QRTZ_PAUSED_TRIGGER_GRPS] PRIMARY KEY CLUSTERED + ( + [SCHED_NAME], + [TRIGGER_GROUP] + ) +GO + +ALTER TABLE [dbo].[QRTZ_SCHEDULER_STATE] WITH NOCHECK ADD + CONSTRAINT [PK_QRTZ_SCHEDULER_STATE] PRIMARY KEY CLUSTERED + ( + [SCHED_NAME], + [INSTANCE_NAME] + ) +GO + +ALTER TABLE [dbo].[QRTZ_LOCKS] WITH NOCHECK ADD + CONSTRAINT [PK_QRTZ_LOCKS] PRIMARY KEY CLUSTERED + ( + [SCHED_NAME], + [LOCK_NAME] + ) +GO + +ALTER TABLE [dbo].[QRTZ_JOB_DETAILS] WITH NOCHECK ADD + CONSTRAINT [PK_QRTZ_JOB_DETAILS] PRIMARY KEY CLUSTERED + ( + [SCHED_NAME], + [JOB_NAME], + [JOB_GROUP] + ) +GO + +ALTER TABLE [dbo].[QRTZ_SIMPLE_TRIGGERS] WITH NOCHECK ADD + CONSTRAINT [PK_QRTZ_SIMPLE_TRIGGERS] PRIMARY KEY CLUSTERED + ( + [SCHED_NAME], + [TRIGGER_NAME], + [TRIGGER_GROUP] + ) +GO + +ALTER TABLE [dbo].[QRTZ_SIMPROP_TRIGGERS] WITH NOCHECK ADD + CONSTRAINT [PK_QRTZ_SIMPROP_TRIGGERS] PRIMARY KEY CLUSTERED + ( + [SCHED_NAME], + [TRIGGER_NAME], + [TRIGGER_GROUP] + ) +GO + +ALTER TABLE [dbo].[QRTZ_TRIGGERS] WITH NOCHECK ADD + CONSTRAINT [PK_QRTZ_TRIGGERS] PRIMARY KEY CLUSTERED + ( + [SCHED_NAME], + [TRIGGER_NAME], + [TRIGGER_GROUP] + ) +GO + +ALTER TABLE [dbo].QRTZ_BLOB_TRIGGERS WITH NOCHECK ADD + CONSTRAINT [PK_QRTZ_BLOB_TRIGGERS] PRIMARY KEY CLUSTERED + ( + [SCHED_NAME], + [TRIGGER_NAME], + [TRIGGER_GROUP] + ) +GO + +ALTER TABLE [dbo].[QRTZ_CRON_TRIGGERS] ADD + CONSTRAINT [FK_QRTZ_CRON_TRIGGERS_QRTZ_TRIGGERS] FOREIGN KEY + ( + [SCHED_NAME], + [TRIGGER_NAME], + [TRIGGER_GROUP] + ) REFERENCES [dbo].[QRTZ_TRIGGERS] ( + [SCHED_NAME], + [TRIGGER_NAME], + [TRIGGER_GROUP] + ) ON DELETE CASCADE +GO + +ALTER TABLE [dbo].[QRTZ_SIMPLE_TRIGGERS] ADD + CONSTRAINT [FK_QRTZ_SIMPLE_TRIGGERS_QRTZ_TRIGGERS] FOREIGN KEY + ( + [SCHED_NAME], + [TRIGGER_NAME], + [TRIGGER_GROUP] + ) REFERENCES [dbo].[QRTZ_TRIGGERS] ( + [SCHED_NAME], + [TRIGGER_NAME], + [TRIGGER_GROUP] + ) ON DELETE CASCADE +GO + +ALTER TABLE [dbo].[QRTZ_SIMPROP_TRIGGERS] ADD + CONSTRAINT [FK_QRTZ_SIMPROP_TRIGGERS_QRTZ_TRIGGERS] FOREIGN KEY + ( + [SCHED_NAME], + [TRIGGER_NAME], + [TRIGGER_GROUP] + ) REFERENCES [dbo].[QRTZ_TRIGGERS] ( + [SCHED_NAME], + [TRIGGER_NAME], + [TRIGGER_GROUP] + ) ON DELETE CASCADE +GO + +ALTER TABLE [dbo].[QRTZ_TRIGGERS] ADD + CONSTRAINT [FK_QRTZ_TRIGGERS_QRTZ_JOB_DETAILS] FOREIGN KEY + ( + [SCHED_NAME], + [JOB_NAME], + [JOB_GROUP] + ) REFERENCES [dbo].[QRTZ_JOB_DETAILS] ( + [SCHED_NAME], + [JOB_NAME], + [JOB_GROUP] + ) +GO + +CREATE INDEX IDX_QRTZ_T_J ON QRTZ_TRIGGERS(SCHED_NAME,JOB_NAME,JOB_GROUP) +CREATE INDEX IDX_QRTZ_T_JG ON QRTZ_TRIGGERS(SCHED_NAME,JOB_GROUP) +CREATE INDEX IDX_QRTZ_T_C ON QRTZ_TRIGGERS(SCHED_NAME,CALENDAR_NAME) +CREATE INDEX IDX_QRTZ_T_G ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_GROUP) +CREATE INDEX IDX_QRTZ_T_STATE ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_STATE) +CREATE INDEX IDX_QRTZ_T_N_STATE ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP,TRIGGER_STATE) +CREATE INDEX IDX_QRTZ_T_N_G_STATE ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_GROUP,TRIGGER_STATE) +CREATE INDEX IDX_QRTZ_T_NEXT_FIRE_TIME ON QRTZ_TRIGGERS(SCHED_NAME,NEXT_FIRE_TIME) +CREATE INDEX IDX_QRTZ_T_NFT_ST ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_STATE,NEXT_FIRE_TIME) +CREATE INDEX IDX_QRTZ_T_NFT_MISFIRE ON QRTZ_TRIGGERS(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME) +CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE ON QRTZ_TRIGGERS(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME,TRIGGER_STATE) +CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE_GRP ON QRTZ_TRIGGERS(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME,TRIGGER_GROUP,TRIGGER_STATE) + +CREATE INDEX IDX_QRTZ_FT_TRIG_INST_NAME ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,INSTANCE_NAME) +CREATE INDEX IDX_QRTZ_FT_INST_JOB_REQ_RCVRY ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,INSTANCE_NAME,REQUESTS_RECOVERY) +CREATE INDEX IDX_QRTZ_FT_J_G ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,JOB_NAME,JOB_GROUP) +CREATE INDEX IDX_QRTZ_FT_JG ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,JOB_GROUP) +CREATE INDEX IDX_QRTZ_FT_T_G ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) +CREATE INDEX IDX_QRTZ_FT_TG ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,TRIGGER_GROUP) +GO \ No newline at end of file diff --git a/src/Net461TopShelf/App.config b/src/Net461TopShelf/App.config new file mode 100644 index 0000000..4f401e3 --- /dev/null +++ b/src/Net461TopShelf/App.config @@ -0,0 +1,29 @@ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Net461TopShelf/Net461TopShelf.csproj b/src/Net461TopShelf/Net461TopShelf.csproj new file mode 100644 index 0000000..023c9a5 --- /dev/null +++ b/src/Net461TopShelf/Net461TopShelf.csproj @@ -0,0 +1,19 @@ + + + + Exe + net461 + + + + + + + + + + + + + + diff --git a/src/Net461TopShelf/Program.cs b/src/Net461TopShelf/Program.cs new file mode 100644 index 0000000..cc4fca9 --- /dev/null +++ b/src/Net461TopShelf/Program.cs @@ -0,0 +1,113 @@ +using Autofac; +using GreenPipes; +using MassTransit; +using MassTransit.QuartzIntegration; +using MassTransit.Scheduling; +using MassTransit.SerilogIntegration; +using Quartz; +using Quartz.Impl; +using Serilog; +using System; +using System.Configuration; +using Topshelf; +using Topshelf.HostConfigurators; +using Topshelf.Logging; + +namespace Net461TopShelf +{ + class Program + { + static int Main() + { + ConfigureSerilog(); + + try + { + return (int)HostFactory.Run(Configure); + } + catch (Exception e) + { + Log.Logger.Fatal(e.ToString()); + throw; + } + } + + static void Configure(HostConfigurator hostConfigurator) + { + hostConfigurator.Service(s => + { + // TopShelf will create a new Service every time you Stop/Start within Windows Services. + s.ConstructUsing(() => + { + var builder = new ContainerBuilder(); + + // Service Bus + builder.AddMassTransit(cfg => + { + cfg.AddBus(ConfigureBus); + }); + + // Should Only ever register one Service + builder.RegisterType() + .SingleInstance(); + + builder.Register(x => new StdSchedulerFactory().GetScheduler().ConfigureAwait(false).GetAwaiter().GetResult()) + .SingleInstance(); + + var container = builder.Build(); + + return container.Resolve(); + }); + + s.WhenStarted((service, control) => service.Start()); + s.WhenStopped((service, control) => service.Stop()); + }); + + hostConfigurator.SetDisplayName("MT.Net461.Scheduler"); + hostConfigurator.SetServiceName("MT.Net461.Scheduler"); + hostConfigurator.SetDescription("MT.Net461.Scheduler"); + } + + static IBusControl ConfigureBus(IComponentContext context) + { + var scheduler = context.Resolve(); + + return Bus.Factory.CreateUsingRabbitMq(cfg => + { + var host = cfg.Host(ConfigurationManager.AppSettings["RabbitMQHost"], ConfigurationManager.AppSettings["RabbitMQVirtualHost"], h => + { + h.Username(ConfigurationManager.AppSettings["RabbitMQUsername"]); + h.Password(ConfigurationManager.AppSettings["RabbitMQPassword"]); + }); + + cfg.UseJsonSerializer(); // Because we are using json within Quartz for serializer type + + cfg.ReceiveEndpoint(host, ConfigurationManager.AppSettings["QueueName"], endpoint => + { + var partitionCount = Environment.ProcessorCount; + endpoint.PrefetchCount = (ushort)(partitionCount); + var partitioner = endpoint.CreatePartitioner(partitionCount); + + endpoint.Consumer(() => new ScheduleMessageConsumer(scheduler), x => + x.Message(m => m.UsePartitioner(partitioner, p => p.Message.CorrelationId))); + endpoint.Consumer(() => new CancelScheduledMessageConsumer(scheduler), + x => x.Message(m => m.UsePartitioner(partitioner, p => p.Message.TokenId))); + }); + }); + } + + static void ConfigureSerilog() + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Debug() + .WriteTo.Console() + .CreateLogger(); + + // Configure Topshelf Logger + SerilogLogWriterFactory.Use(Log.Logger); + + // MassTransit to use Serilog + SerilogLogger.Use(Log.Logger); + } + } +} diff --git a/src/Net461TopShelf/SchedulerService.cs b/src/Net461TopShelf/SchedulerService.cs new file mode 100644 index 0000000..217cd44 --- /dev/null +++ b/src/Net461TopShelf/SchedulerService.cs @@ -0,0 +1,55 @@ +using MassTransit; +using MassTransit.QuartzIntegration; +using MassTransit.Util; +using Quartz; +using Quartz.Impl; +using System; + +namespace Net461TopShelf +{ + public class SchedulerService + { + IScheduler _scheduler; + IBusControl _busControl; + BusHandle _busHandle; + + public SchedulerService(IBusControl busControl, IScheduler scheduler) + { + _busControl = busControl; + _scheduler = scheduler; + } + + public bool Start() + { + try + { + _busHandle = TaskUtil.Await(() => _busControl.StartAsync()); + + _scheduler.JobFactory = new MassTransitJobFactory(_busControl); + + TaskUtil.Await(() => _scheduler.Start()); + } + catch (Exception) + { + TaskUtil.Await(() => _scheduler.Shutdown()); + throw; + } + + Console.WriteLine("Started"); + return true; + } + + public bool Stop() + { + TaskUtil.Await(() => _scheduler.Standby()); + + if (_busHandle != null) + TaskUtil.Await(() => _busHandle.StopAsync()); + + TaskUtil.Await(() => _scheduler.Shutdown()); + + Console.WriteLine("Stopped"); + return true; + } + } +} diff --git a/src/NetCore/AppConfig.cs b/src/NetCore/AppConfig.cs new file mode 100644 index 0000000..2f755f6 --- /dev/null +++ b/src/NetCore/AppConfig.cs @@ -0,0 +1,11 @@ +namespace NetCore +{ + public class AppConfig + { + public string Host { get; set; } + public string VirtualHost { get; set; } + public string Username { get; set; } + public string Password { get; set; } + public string QueueName { get; set; } + } +} diff --git a/src/NetCore/MassTransitConsoleHostedService.cs b/src/NetCore/MassTransitConsoleHostedService.cs new file mode 100644 index 0000000..1a81339 --- /dev/null +++ b/src/NetCore/MassTransitConsoleHostedService.cs @@ -0,0 +1,63 @@ +using MassTransit; +using MassTransit.Logging.Tracing; +using MassTransit.QuartzIntegration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Quartz; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace NetCore +{ + public class MassTransitConsoleHostedService : IHostedService + { + readonly IBusControl _bus; + readonly ILogger _logger; + IScheduler _scheduler; + + public MassTransitConsoleHostedService(IBusControl bus, ILoggerFactory loggerFactory, IScheduler scheduler) + { + _bus = bus; + + if (loggerFactory != null && MassTransit.Logging.Logger.Current.GetType() == typeof(TraceLogger)) + MassTransit.ExtensionsLoggingIntegration.ExtensionsLogger.Use(loggerFactory); + _logger = loggerFactory.CreateLogger(); + + _scheduler = scheduler; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Starting bus"); + await _bus.StartAsync(cancellationToken).ConfigureAwait(false); + + _scheduler.JobFactory = new MassTransitJobFactory(_bus); + try + { + _logger.LogInformation("Starting scheduler"); + await _scheduler.Start(); + } + catch (Exception) + { + await _scheduler.Shutdown(); + + throw; + } + + _logger.LogInformation("Started"); + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + await _scheduler.Standby(); + + _logger.LogInformation("Stopping"); + await _bus.StopAsync(cancellationToken); + + await _scheduler.Shutdown(); + + _logger.LogInformation("Stopped"); + } + } +} diff --git a/src/NetCore/NetCore.csproj b/src/NetCore/NetCore.csproj new file mode 100644 index 0000000..49a1c09 --- /dev/null +++ b/src/NetCore/NetCore.csproj @@ -0,0 +1,34 @@ + + + + Exe + netcoreapp2.2 + latest + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + + diff --git a/src/NetCore/Program.cs b/src/NetCore/Program.cs new file mode 100644 index 0000000..fe05352 --- /dev/null +++ b/src/NetCore/Program.cs @@ -0,0 +1,87 @@ +using GreenPipes; +using MassTransit; +using MassTransit.QuartzIntegration; +using MassTransit.Scheduling; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Quartz; +using Quartz.Impl; +using System; +using System.Threading.Tasks; + +namespace NetCore +{ + class Program + { + static async Task Main(string[] args) + { + var builder = CreateHostBuilder(args); + + await builder.RunConsoleAsync(); + } + + static IHostBuilder CreateHostBuilder(string[] args) => + new HostBuilder() + .ConfigureAppConfiguration((hostingContext, config) => + { + config.AddJsonFile("appsettings.json", optional: true); + config.AddEnvironmentVariables(); + + if (args != null) + config.AddCommandLine(args); + }) + .ConfigureServices((hostContext, services) => + { + services.Configure(hostContext.Configuration.GetSection("AppConfig")); + + // Service Bus + services.AddMassTransit(cfg => + { + cfg.AddBus(ConfigureBus); + }); + + services.AddHostedService(); + + services.AddSingleton(x => new StdSchedulerFactory().GetScheduler().ConfigureAwait(false).GetAwaiter().GetResult()); + }) + .ConfigureLogging((hostingContext, logging) => + { + logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); + logging.AddConsole(); + }); + + + + static IBusControl ConfigureBus(IServiceProvider provider) + { + var options = provider.GetRequiredService>().Value; + var scheduler = provider.GetRequiredService(); + + return Bus.Factory.CreateUsingRabbitMq(cfg => + { + var host = cfg.Host(options.Host, options.VirtualHost, h => + { + h.Username(options.Username); + h.Password(options.Password); + }); + + cfg.UseJsonSerializer(); // Because we are using json within Quartz for serializer type + + cfg.ReceiveEndpoint(host, options.QueueName, endpoint => + { + var partitionCount = Environment.ProcessorCount; + endpoint.PrefetchCount = (ushort)(partitionCount); + var partitioner = endpoint.CreatePartitioner(partitionCount); + + endpoint.Consumer(() => new ScheduleMessageConsumer(scheduler), x => + x.Message(m => m.UsePartitioner(partitioner, p => p.Message.CorrelationId))); + endpoint.Consumer(() => new CancelScheduledMessageConsumer(scheduler), + x => x.Message(m => m.UsePartitioner(partitioner, p => p.Message.TokenId))); + }); + }); + } + } +} diff --git a/src/NetCore/appsettings.json b/src/NetCore/appsettings.json new file mode 100644 index 0000000..ffd775c --- /dev/null +++ b/src/NetCore/appsettings.json @@ -0,0 +1,14 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug" + } + }, + "AppConfig": { + "Host": "localhost", + "VirtualHost": "/", + "Username": "guest", + "Password": "guest", + "QueueName": "quartz-scheduler" + } +} \ No newline at end of file diff --git a/src/NetCore/quartz.config b/src/NetCore/quartz.config new file mode 100644 index 0000000..cab3710 --- /dev/null +++ b/src/NetCore/quartz.config @@ -0,0 +1,13 @@ +quartz.scheduler.instanceName = MassTransit-Scheduler +quartz.scheduler.instanceId = AUTO +quartz.serializer.type = json +quartz.threadPool.type = Quartz.Simpl.SimpleThreadPool, Quartz +quartz.threadPool.threadCount = 10 +quartz.jobStore.misfireThreshold = 60000 +quartz.jobStore.type = Quartz.Impl.AdoJobStore.JobStoreTX, Quartz +quartz.jobStore.driverDelegateType = Quartz.Impl.AdoJobStore.SqlServerDelegate, Quartz +quartz.jobStore.tablePrefix = QRTZ_ +quartz.jobStore.dataSource = quartzDS +quartz.dataSource.quartzDS.connectionString = Data Source=(LocalDb)\MSSQLLocalDB;Initial Catalog=mt-scheduler;Trusted_Connection=True;MultipleActiveResultSets=True;Connection Timeout=30 +quartz.dataSource.quartzDS.provider = SqlServer +quartz.jobStore.useProperties = true \ No newline at end of file diff --git a/src/NetCoreWinSvc/AppConfig.cs b/src/NetCoreWinSvc/AppConfig.cs new file mode 100644 index 0000000..6fcc840 --- /dev/null +++ b/src/NetCoreWinSvc/AppConfig.cs @@ -0,0 +1,11 @@ +namespace NetCoreWinSvc +{ + public class AppConfig + { + public string Host { get; set; } + public string VirtualHost { get; set; } + public string Username { get; set; } + public string Password { get; set; } + public string QueueName { get; set; } + } +} diff --git a/src/NetCoreWinSvc/MassTransitConsoleHostedService.cs b/src/NetCoreWinSvc/MassTransitConsoleHostedService.cs new file mode 100644 index 0000000..466d02f --- /dev/null +++ b/src/NetCoreWinSvc/MassTransitConsoleHostedService.cs @@ -0,0 +1,63 @@ +using MassTransit; +using MassTransit.Logging.Tracing; +using MassTransit.QuartzIntegration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Quartz; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace NetCoreWinSvc +{ + public class MassTransitConsoleHostedService : IHostedService + { + readonly IBusControl _bus; + readonly ILogger _logger; + IScheduler _scheduler; + + public MassTransitConsoleHostedService(IBusControl bus, ILoggerFactory loggerFactory, IScheduler scheduler) + { + _bus = bus; + + if (loggerFactory != null && MassTransit.Logging.Logger.Current.GetType() == typeof(TraceLogger)) + MassTransit.ExtensionsLoggingIntegration.ExtensionsLogger.Use(loggerFactory); + _logger = loggerFactory.CreateLogger(); + + _scheduler = scheduler; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Starting bus"); + await _bus.StartAsync(cancellationToken).ConfigureAwait(false); + + _scheduler.JobFactory = new MassTransitJobFactory(_bus); + try + { + _logger.LogInformation("Starting scheduler"); + await _scheduler.Start(); + } + catch(Exception) + { + await _scheduler.Shutdown(); + + throw; + } + + _logger.LogInformation("Started"); + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + await _scheduler.Standby(); + + _logger.LogInformation("Stopping"); + await _bus.StopAsync(cancellationToken); + + await _scheduler.Shutdown(); + + _logger.LogInformation("Stopped"); + } + } +} diff --git a/src/NetCoreWinSvc/NetCoreWinSvc.csproj b/src/NetCoreWinSvc/NetCoreWinSvc.csproj new file mode 100644 index 0000000..9f5f607 --- /dev/null +++ b/src/NetCoreWinSvc/NetCoreWinSvc.csproj @@ -0,0 +1,35 @@ + + + + Exe + netcoreapp2.2 + latest + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + + diff --git a/src/NetCoreWinSvc/Program.cs b/src/NetCoreWinSvc/Program.cs new file mode 100644 index 0000000..e2f0d56 --- /dev/null +++ b/src/NetCoreWinSvc/Program.cs @@ -0,0 +1,98 @@ +using GreenPipes; +using MassTransit; +using MassTransit.QuartzIntegration; +using MassTransit.Scheduling; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Quartz; +using Quartz.Impl; +using System; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; + +namespace NetCoreWinSvc +{ + class Program + { + static async Task Main(string[] args) + { + var isService = !(Debugger.IsAttached || args.Contains("--console")); + + var builder = CreateHostBuilder(args); + + if (isService) + { + await builder.RunAsServiceAsync(); + } + else + { + await builder.RunConsoleAsync(); + } + } + + static IHostBuilder CreateHostBuilder(string[] args) => + new HostBuilder() + .ConfigureAppConfiguration((hostingContext, config) => + { + config.AddJsonFile("appsettings.json", optional: true); + config.AddEnvironmentVariables(); + + if (args != null) + config.AddCommandLine(args); + }) + .ConfigureServices((hostContext, services) => + { + services.Configure(hostContext.Configuration.GetSection("AppConfig")); + + // Service Bus + services.AddMassTransit(cfg => + { + cfg.AddBus(ConfigureBus); + }); + + services.AddHostedService(); + + services.AddSingleton(x => new StdSchedulerFactory().GetScheduler().ConfigureAwait(false).GetAwaiter().GetResult()); + }) + .ConfigureLogging((hostingContext, logging) => + { + logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); + logging.AddConsole(); + }); + + + + static IBusControl ConfigureBus(IServiceProvider provider) + { + var options = provider.GetRequiredService>().Value; + var scheduler = provider.GetRequiredService(); + + return Bus.Factory.CreateUsingRabbitMq(cfg => + { + var host = cfg.Host(options.Host, options.VirtualHost, h => + { + h.Username(options.Username); + h.Password(options.Password); + }); + + cfg.UseJsonSerializer(); // Because we are using json within Quartz for serializer type + + cfg.ReceiveEndpoint(host, options.QueueName, endpoint => + { + var partitionCount = Environment.ProcessorCount; + endpoint.PrefetchCount = (ushort)(partitionCount); + var partitioner = endpoint.CreatePartitioner(partitionCount); + + endpoint.Consumer(() => new ScheduleMessageConsumer(scheduler), x => + x.Message(m => m.UsePartitioner(partitioner, p => p.Message.CorrelationId))); + endpoint.Consumer(() => new CancelScheduledMessageConsumer(scheduler), + x => x.Message(m => m.UsePartitioner(partitioner, p => p.Message.TokenId))); + }); + }); + } + } +} diff --git a/src/NetCoreWinSvc/ServiceBaseLifetime.cs b/src/NetCoreWinSvc/ServiceBaseLifetime.cs new file mode 100644 index 0000000..c9374d1 --- /dev/null +++ b/src/NetCoreWinSvc/ServiceBaseLifetime.cs @@ -0,0 +1,63 @@ +using Microsoft.Extensions.Hosting; +using System; +using System.ServiceProcess; +using System.Threading; +using System.Threading.Tasks; + +namespace NetCoreWinSvc +{ + public class ServiceBaseLifetime : ServiceBase, IHostLifetime + { + private readonly TaskCompletionSource _delayStart = new TaskCompletionSource(); + + public ServiceBaseLifetime(IApplicationLifetime applicationLifetime) + { + ApplicationLifetime = applicationLifetime ?? throw new ArgumentNullException(nameof(applicationLifetime)); + } + + private IApplicationLifetime ApplicationLifetime { get; } + + public Task WaitForStartAsync(CancellationToken cancellationToken) + { + cancellationToken.Register(() => _delayStart.TrySetCanceled()); + ApplicationLifetime.ApplicationStopping.Register(Stop); + + new Thread(Run).Start(); // Otherwise this would block and prevent IHost.StartAsync from finishing. + return _delayStart.Task; + } + + private void Run() + { + try + { + Run(this); // This blocks until the service is stopped. + _delayStart.TrySetException(new InvalidOperationException("Stopped without starting")); + } + catch (Exception ex) + { + _delayStart.TrySetException(ex); + } + } + + public Task StopAsync(CancellationToken cancellationToken) + { + Stop(); + return Task.CompletedTask; + } + + // Called by base.Run when the service is ready to start. + protected override void OnStart(string[] args) + { + _delayStart.TrySetResult(null); + base.OnStart(args); + } + + // Called by base.Stop. This may be called multiple times by service Stop, ApplicationStopping, and StopAsync. + // That's OK because StopApplication uses a CancellationTokenSource and prevents any recursion. + protected override void OnStop() + { + ApplicationLifetime.StopApplication(); + base.OnStop(); + } + } +} diff --git a/src/NetCoreWinSvc/ServiceBaseLifetimeHostExtensions.cs b/src/NetCoreWinSvc/ServiceBaseLifetimeHostExtensions.cs new file mode 100644 index 0000000..1f92778 --- /dev/null +++ b/src/NetCoreWinSvc/ServiceBaseLifetimeHostExtensions.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System.Threading; +using System.Threading.Tasks; + +namespace NetCoreWinSvc +{ + public static class ServiceBaseLifetimeHostExtensions + { + public static IHostBuilder UseServiceBaseLifetime(this IHostBuilder hostBuilder) + { + return hostBuilder.ConfigureServices((hostContext, services) => services.AddSingleton()); + } + + public static Task RunAsServiceAsync(this IHostBuilder hostBuilder, CancellationToken cancellationToken = default) + { + return hostBuilder.UseServiceBaseLifetime().Build().RunAsync(cancellationToken); + } + } +} diff --git a/src/NetCoreWinSvc/appsettings.json b/src/NetCoreWinSvc/appsettings.json new file mode 100644 index 0000000..ffd775c --- /dev/null +++ b/src/NetCoreWinSvc/appsettings.json @@ -0,0 +1,14 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug" + } + }, + "AppConfig": { + "Host": "localhost", + "VirtualHost": "/", + "Username": "guest", + "Password": "guest", + "QueueName": "quartz-scheduler" + } +} \ No newline at end of file diff --git a/src/NetCoreWinSvc/quartz.config b/src/NetCoreWinSvc/quartz.config new file mode 100644 index 0000000..cab3710 --- /dev/null +++ b/src/NetCoreWinSvc/quartz.config @@ -0,0 +1,13 @@ +quartz.scheduler.instanceName = MassTransit-Scheduler +quartz.scheduler.instanceId = AUTO +quartz.serializer.type = json +quartz.threadPool.type = Quartz.Simpl.SimpleThreadPool, Quartz +quartz.threadPool.threadCount = 10 +quartz.jobStore.misfireThreshold = 60000 +quartz.jobStore.type = Quartz.Impl.AdoJobStore.JobStoreTX, Quartz +quartz.jobStore.driverDelegateType = Quartz.Impl.AdoJobStore.SqlServerDelegate, Quartz +quartz.jobStore.tablePrefix = QRTZ_ +quartz.jobStore.dataSource = quartzDS +quartz.dataSource.quartzDS.connectionString = Data Source=(LocalDb)\MSSQLLocalDB;Initial Catalog=mt-scheduler;Trusted_Connection=True;MultipleActiveResultSets=True;Connection Timeout=30 +quartz.dataSource.quartzDS.provider = SqlServer +quartz.jobStore.useProperties = true \ No newline at end of file diff --git a/src/Sample-Quartz.sln b/src/Sample-Quartz.sln new file mode 100644 index 0000000..9a8d543 --- /dev/null +++ b/src/Sample-Quartz.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.28307.329 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetCore", "NetCore\NetCore.csproj", "{E0E94665-623A-4CA2-99AD-D9103E17C63F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetCoreWinSvc", "NetCoreWinSvc\NetCoreWinSvc.csproj", "{B1A033D3-4D4F-4A1E-8BE7-BFF5675E2650}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Net461TopShelf", "Net461TopShelf\Net461TopShelf.csproj", "{EFA36BE4-C5FF-4492-ADD0-F5E758BD1758}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E0E94665-623A-4CA2-99AD-D9103E17C63F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E0E94665-623A-4CA2-99AD-D9103E17C63F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E0E94665-623A-4CA2-99AD-D9103E17C63F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E0E94665-623A-4CA2-99AD-D9103E17C63F}.Release|Any CPU.Build.0 = Release|Any CPU + {B1A033D3-4D4F-4A1E-8BE7-BFF5675E2650}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B1A033D3-4D4F-4A1E-8BE7-BFF5675E2650}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B1A033D3-4D4F-4A1E-8BE7-BFF5675E2650}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B1A033D3-4D4F-4A1E-8BE7-BFF5675E2650}.Release|Any CPU.Build.0 = Release|Any CPU + {EFA36BE4-C5FF-4492-ADD0-F5E758BD1758}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EFA36BE4-C5FF-4492-ADD0-F5E758BD1758}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EFA36BE4-C5FF-4492-ADD0-F5E758BD1758}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EFA36BE4-C5FF-4492-ADD0-F5E758BD1758}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {F7A73612-3C1E-4FBD-977F-62D13E38A87E} + EndGlobalSection +EndGlobal