次のクラスがあります
_ public class MyEmailService
{
public async Task<bool> SendAdminEmails()
{
...
}
public async Task<bool> SendUserEmails()
{
...
}
}
public interface IMyEmailService
{
Task<bool> SendAdminEmails();
Task<bool> SendUserEmails();
}
_
最新の Quartz 2.4.1 Nugetパッケージ をインストールしました。これは、別個のSQL ServerデータベースなしでWebアプリに軽量のスケジューラーが必要だったためです。
メソッドをスケジュールする必要があります
SendUserEmails
は、毎週月曜日17:00、火曜日17:00および水曜日17:00に実行されますSendAdminEmails
は毎週木曜日09:00、金曜日9:00に実行しますASP.NET CoreのQuartzを使用してこれらのメソッドをスケジュールするには、どのコードが必要ですか?また、インターネット上のすべてのコードサンプルは以前のバージョンのASP.NETを参照しているため、ASP.NET CoreでQuartzを起動する方法を知る必要があります。
コードサンプルを見つける 以前のバージョンのASP.NETについてはできますが、ASP.NET CoreでQuartzを起動してテストを開始する方法がわかりません。 ASP.NET CoreのJobScheduler.Start();
はどこに配置しますか?
想定されるツール:Visual Studio 2017 RTM、.NET Core 1.1、.NET Core SDK 1.0、SQL Server Express 2016 LocalDB。
Webアプリケーション.csprojの場合:
<Project Sdk="Microsoft.NET.Sdk.Web">
<!-- .... existing contents .... -->
<!-- add the following ItemGroup element, it adds required packages -->
<ItemGroup>
<PackageReference Include="Quartz" Version="3.0.0-alpha2" />
<PackageReference Include="Quartz.Serialization.Json" Version="3.0.0-alpha2" />
</ItemGroup>
</Project>
Program
クラス(デフォルトではVisual Studioによって足場化されている):
public class Program
{
private static IScheduler _scheduler; // add this field
public static void Main(string[] args)
{
var Host = new WebHostBuilder()
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.UseApplicationInsights()
.Build();
StartScheduler(); // add this line
Host.Run();
}
// add this method
private static void StartScheduler()
{
var properties = new NameValueCollection {
// json serialization is the one supported under .NET Core (binary isn't)
["quartz.serializer.type"] = "json",
// the following setup of job store is just for example and it didn't change from v2
// according to your usage scenario though, you definitely need
// the ADO.NET job store and not the RAMJobStore.
["quartz.jobStore.type"] = "Quartz.Impl.AdoJobStore.JobStoreTX, Quartz",
["quartz.jobStore.useProperties"] = "false",
["quartz.jobStore.dataSource"] = "default",
["quartz.jobStore.tablePrefix"] = "QRTZ_",
["quartz.jobStore.driverDelegateType"] = "Quartz.Impl.AdoJobStore.SqlServerDelegate, Quartz",
["quartz.dataSource.default.provider"] = "SqlServer-41", // SqlServer-41 is the new provider for .NET Core
["quartz.dataSource.default.connectionString"] = @"Server=(localdb)\MSSQLLocalDB;Database=Quartz;Integrated Security=true"
};
var schedulerFactory = new StdSchedulerFactory(properties);
_scheduler = schedulerFactory.GetScheduler().Result;
_scheduler.Start().Wait();
var userEmailsJob = JobBuilder.Create<SendUserEmailsJob>()
.WithIdentity("SendUserEmails")
.Build();
var userEmailsTrigger = TriggerBuilder.Create()
.WithIdentity("UserEmailsCron")
.StartNow()
.WithCronSchedule("0 0 17 ? * MON,TUE,WED")
.Build();
_scheduler.ScheduleJob(userEmailsJob, userEmailsTrigger).Wait();
var adminEmailsJob = JobBuilder.Create<SendAdminEmailsJob>()
.WithIdentity("SendAdminEmails")
.Build();
var adminEmailsTrigger = TriggerBuilder.Create()
.WithIdentity("AdminEmailsCron")
.StartNow()
.WithCronSchedule("0 0 9 ? * THU,FRI")
.Build();
_scheduler.ScheduleJob(adminEmailsJob, adminEmailsTrigger).Wait();
}
}
ジョブクラスの例:
public class SendUserEmailsJob : IJob
{
public Task Execute(IJobExecutionContext context)
{
// an instance of email service can be obtained in different ways,
// e.g. service locator, constructor injection (requires custom job factory)
IMyEmailService emailService = new MyEmailService();
// delegate the actual work to email service
return emailService.SendUserEmails();
}
}
まず、 この発表 に従って、.NET Coreをターゲットとするため、Quartzのv3を使用する必要があります。
現在、v3パッケージのアルファバージョンのみが NuGetで利用可能 です。チームは、.5.0 Coreを対象としない2.5.0のリリースに多大な努力を払ったようです。それにもかかわらず、GitHubリポジトリでは、master
ブランチはすでにv3専用であり、基本的に、 v3リリースの未解決の問題 は重要ではないようです。最近の commit activity は非常に低いため、v3のリリースは数か月間、または半年後になると予想していますが、誰も知りません。
WebアプリケーションをIISでホストする場合、ワーカープロセスのリサイクル/アンロード動作を考慮する必要があります。 ASP.NET Core Webアプリは、w3wp.exeとは別に、通常の.NET Coreプロセスとして実行されます-IISはリバースプロキシとしてのみ機能します。それでも、w3wp.exeのインスタンスがリサイクルまたはアンロードされると、関連する.NET Coreアプリプロセスも終了するように通知されます( this に従って)。
Webアプリケーションは、IIS以外のリバースプロキシ(NGINXなど)の背後で自己ホストすることもできますが、IISを使用していると想定し、それに応じて回答を絞り込みます。
リサイクル/アンロードがもたらす問題は、 @ darin-dimitrovが参照する投稿 で詳しく説明されています。
上記の問題にもかかわらず、これらの電子メールジョブをWebアプリでホストすることの正当化を考えることができます。 1種類のアプリケーションモデル(ASP.NET)のみを使用することが決定されました。このようなアプローチにより、学習曲線、展開手順、生産監視などが簡素化されます。
バックエンドマイクロサービス(電子メールジョブを移動するのに適した場所)を導入したくない場合は、IISリサイクル/アンロード動作を克服し、Webアプリ内でQuartzを実行するのが理にかなっています。
または、他の理由があるかもしれません。
シナリオでは、ジョブ実行のステータスをプロセス外で保持する必要があります。したがって、デフォルトのRAMJobStoreは適合せず、ADO.NET Job Store。
質問でSQL Serverに言及したので、SQL Serverデータベースのセットアップ例を示します。
Visual Studio 2017と.NET Coreツールの最新/最新バージョンを使用すると仮定します。私のものは.NET Core Runtime 1.1および.NET Core SDK 1.0です。
DBセットアップの例では、SQL Server 2016 Express LocalDBでQuartz
という名前のデータベースを使用します。 DBセットアップスクリプトは こちらにあります です。
まず、必要なパッケージ参照をWebアプリケーションの.csprojに追加します(またはVisual StudioのNuGetパッケージマネージャーGUIで行います)。
<Project Sdk="Microsoft.NET.Sdk.Web">
<!-- .... existing contents .... -->
<!-- the following ItemGroup adds required packages -->
<ItemGroup>
<PackageReference Include="Quartz" Version="3.0.0-alpha2" />
<PackageReference Include="Quartz.Serialization.Json" Version="3.0.0-alpha2" />
</ItemGroup>
</Project>
Migration Guide と V3 Tutorial の助けを借りて、スケジューラーを開始および停止する方法を見つけることができます。これを別のクラスにカプセル化することを好みます。名前をQuartzStartup
にしましょう。
using System;
using System.Collections.Specialized;
using System.Threading.Tasks;
using Quartz;
using Quartz.Impl;
namespace WebApplication1
{
// Responsible for starting and gracefully stopping the scheduler.
public class QuartzStartup
{
private IScheduler _scheduler; // after Start, and until shutdown completes, references the scheduler object
// starts the scheduler, defines the jobs and the triggers
public void Start()
{
if (_scheduler != null)
{
throw new InvalidOperationException("Already started.");
}
var properties = new NameValueCollection {
// json serialization is the one supported under .NET Core (binary isn't)
["quartz.serializer.type"] = "json",
// the following setup of job store is just for example and it didn't change from v2
["quartz.jobStore.type"] = "Quartz.Impl.AdoJobStore.JobStoreTX, Quartz",
["quartz.jobStore.useProperties"] = "false",
["quartz.jobStore.dataSource"] = "default",
["quartz.jobStore.tablePrefix"] = "QRTZ_",
["quartz.jobStore.driverDelegateType"] = "Quartz.Impl.AdoJobStore.SqlServerDelegate, Quartz",
["quartz.dataSource.default.provider"] = "SqlServer-41", // SqlServer-41 is the new provider for .NET Core
["quartz.dataSource.default.connectionString"] = @"Server=(localdb)\MSSQLLocalDB;Database=Quartz;Integrated Security=true"
};
var schedulerFactory = new StdSchedulerFactory(properties);
_scheduler = schedulerFactory.GetScheduler().Result;
_scheduler.Start().Wait();
var userEmailsJob = JobBuilder.Create<SendUserEmailsJob>()
.WithIdentity("SendUserEmails")
.Build();
var userEmailsTrigger = TriggerBuilder.Create()
.WithIdentity("UserEmailsCron")
.StartNow()
.WithCronSchedule("0 0 17 ? * MON,TUE,WED")
.Build();
_scheduler.ScheduleJob(userEmailsJob, userEmailsTrigger).Wait();
var adminEmailsJob = JobBuilder.Create<SendAdminEmailsJob>()
.WithIdentity("SendAdminEmails")
.Build();
var adminEmailsTrigger = TriggerBuilder.Create()
.WithIdentity("AdminEmailsCron")
.StartNow()
.WithCronSchedule("0 0 9 ? * THU,FRI")
.Build();
_scheduler.ScheduleJob(adminEmailsJob, adminEmailsTrigger).Wait();
}
// initiates shutdown of the scheduler, and waits until jobs exit gracefully (within allotted timeout)
public void Stop()
{
if (_scheduler == null)
{
return;
}
// give running jobs 30 sec (for example) to stop gracefully
if (_scheduler.Shutdown(waitForJobsToComplete: true).Wait(30000))
{
_scheduler = null;
}
else
{
// jobs didn't exit in timely fashion - log a warning...
}
}
}
}
注1.上記の例では、SendUserEmailsJob
とSendAdminEmailsJob
はIJob
を実装するクラスです。 IJob
インターフェイスは、Task<bool>
ではなくvoid IMyEmailService
を返すため、Task
とは少し異なります。両方のジョブクラスは、IMyEmailService
を依存関係として取得する必要があります(おそらくコンストラクター注入)。
注2.長時間実行されるジョブがタイムリーに終了できるようにするには、IJob.Execute
メソッドでIJobExecutionContext.CancellationToken
のステータスを監視する必要があります。メソッドがIMyEmailService
パラメータを受け取るようにするには、CancellationToken
インターフェイスの変更が必要になる場合があります。
public interface IMyEmailService
{
Task<bool> SendAdminEmails(CancellationToken cancellation);
Task<bool> SendUserEmails(CancellationToken cancellation);
}
ASP.NET Coreでは、アプリケーションbootstrapコードはクラスProgram
に存在します。これはコンソールアプリとよく似ています。 Main
メソッドは、Webホストを作成して実行し、終了するまで待機するために呼び出されます。
public class Program
{
public static void Main(string[] args)
{
var Host = new WebHostBuilder()
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.UseApplicationInsights()
.Build();
Host.Run();
}
}
最も簡単なことは、TL; DRで行ったように、Main
メソッドでQuartzStartup.Start
を呼び出すだけです。しかし、プロセスのシャットダウンも適切に処理する必要があるため、起動コードとシャットダウンコードの両方をより一貫した方法でフックすることを好みます。
この行:
.UseStartup<Startup>()
visual Studioで新しいASP.NET Core Web Applicationプロジェクトを作成するときに足場となるStartup
という名前のクラスを参照します。 Startup
クラスは次のようになります。
public class Startup
{
public Startup(IHostingEnvironment env)
{
// scaffolded code...
}
public IConfigurationRoot Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// scaffolded code...
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
// scaffolded code...
}
}
QuartzStartup.Start
への呼び出しは、Startup
クラスのメソッドの1つに挿入する必要があることは明らかです。問題は、QuartzStartup.Stop
をフックする場所です。
従来の.NET Frameworkでは、ASP.NETはIRegisteredObject
インターフェイスを提供していました。 この投稿 、および ドキュメント によると、ASP.NET CoreではIApplicationLifetime
に置き換えられました。ビンゴ。 IApplicationLifetime
のインスタンスは、パラメーターを介してStartup.Configure
メソッドに注入できます。
一貫性を保つために、QuartzStartup.Start
とQuartzStartup.Stop
の両方をIApplicationLifetime
にフックします。
public class Startup
{
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(
IApplicationBuilder app,
IHostingEnvironment env,
ILoggerFactory loggerFactory,
IApplicationLifetime lifetime) // added this parameter
{
// the following 3 lines hook QuartzStartup into web Host lifecycle
var quartz = new QuartzStartup();
lifetime.ApplicationStarted.Register(quartz.Start);
lifetime.ApplicationStopping.Register(quartz.Stop);
// .... original scaffolded code here ....
}
// ....the rest of the scaffolded members ....
}
Configure
メソッドのシグネチャを追加のIApplicationLifetime
パラメーターで拡張していることに注意してください。 documentation によれば、ApplicationStopping
は登録されたコールバックが完了するまでブロックします。
最新のASP.NET CoreモジュールがインストールされているIISでのみ、IApplicationLifetime.ApplicationStopping
フックの予想される動作を観察することができました。 IIS Express(Visual Studio 2017 Community RTMと共にインストール)と、ASP.NET Coreモジュールの古いバージョンのIISの両方が、一貫してIApplicationLifetime.ApplicationStopping
を呼び出しませんでした。 このバグ が原因で修正されたと思います。
ASP.NET Coreモジュールの最新バージョンをインストールできます from here 。 「最新のASP.NETコアモジュールのインストール」セクションの指示に従ってください。
また、@ Brice Molestiが代替ライブラリとして提案したFluentSchedulerも調べました。私の第一印象では、FluentSchedulerはQuartzと比較して非常に単純で未熟なソリューションです。たとえば、FluentSchedulerは、ジョブステータスの永続化やクラスター化された実行などの基本的な機能を提供しません。
@ felix-bの答えに加えて。ジョブにDIを追加します。 QuartzStartup Startを非同期にすることもできます。
この回答に基づいて: https://stackoverflow.com/a/42158004/123539
public class QuartzStartup
{
public QuartzStartup(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public async Task Start()
{
// other code is same
_scheduler = await schedulerFactory.GetScheduler();
_scheduler.JobFactory = new JobFactory(_serviceProvider);
await _scheduler.Start();
var sampleJob = JobBuilder.Create<SampleJob>().Build();
var sampleTrigger = TriggerBuilder.Create().StartNow().WithCronSchedule("0 0/1 * * * ?").Build();
await _scheduler.ScheduleJob(sampleJob, sampleTrigger);
}
}
JobFactoryクラス
public class JobFactory : IJobFactory
{
private IServiceProvider _serviceProvider;
public JobFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
{
return _serviceProvider.GetService(bundle.JobDetail.JobType) as IJob;
}
public void ReturnJob(IJob job)
{
(job as IDisposable)?.Dispose();
}
}
スタートアップクラス:
public void ConfigureServices(IServiceCollection services)
{
// other code is removed for brevity
// need to register all JOBS by their class name
services.AddTransient<SampleJob>();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApplicationLifetime applicationLifetime)
{
var quartz = new QuartzStartup(_services.BuildServiceProvider());
applicationLifetime.ApplicationStarted.Register(() => quartz.Start());
applicationLifetime.ApplicationStopping.Register(quartz.Stop);
// other code removed for brevity
}
コンストラクター依存性注入を使用したSampleJobクラス:
public class SampleJob : IJob
{
private readonly ILogger<SampleJob> _logger;
public SampleJob(ILogger<SampleJob> logger)
{
_logger = logger;
}
public async Task Execute(IJobExecutionContext context)
{
_logger.LogDebug("Execute called");
}
}
Quartzでそれを行う方法はわかりませんが、非常にうまく機能する他のライブラリで同じシナリオを実験しました。ここでどうやってやった
FluentSchedulerをインストールする
Install-Package FluentScheduler
このように使用します
var registry = new Registry();
JobManager.Initialize(registry);
JobManager.AddJob(() => MyEmailService.SendAdminEmails(), s => s
.ToRunEvery(1)
.Weeks()
.On(DayOfWeek.Monday)
.At(17, 00));
JobManager.AddJob(() => MyEmailService.SendAdminEmails(), s => s
.ToRunEvery(1)
.Weeks()
.On(DayOfWeek.Wednesday)
.At(17, 00));
JobManager.AddJob(() => MyEmailService.SendUserEmails(), s => s
.ToRunEvery(1)
.Weeks()
.On(DayOfWeek.Thursday)
.At(09, 00));
JobManager.AddJob(() => MyEmailService.SendUserEmails(), s => s
.ToRunEvery(1)
.Weeks()
.On(DayOfWeek.Friday)
.At(09, 00));
ドキュメントはここにあります GitHubのFluentScheduler
受け入れられた回答はこのトピックを非常によくカバーしていますが、最新のQuartzバージョンではいくつかの点が変更されています。以下は この記事 に基づいています。Quartz3.0.xおよびASP.NET Core 2.2のクイックスタートを示しています。
public class QuartzJobFactory : IJobFactory
{
private readonly IServiceProvider _serviceProvider;
public QuartzJobFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
{
var jobDetail = bundle.JobDetail;
var job = (IJob)_serviceProvider.GetService(jobDetail.JobType);
return job;
}
public void ReturnJob(IJob job) { }
}
[DisallowConcurrentExecution]
public class TestJob : IJob
{
private ILoggingService Logger { get; }
private IApplicationLifetime ApplicationLifetime { get; }
private static object lockHandle = new object();
private static bool shouldExit = false;
public TestJob(ILoggingService loggingService, IApplicationLifetime applicationLifetime)
{
Logger = loggingService;
ApplicationLifetime = applicationLifetime;
}
public Task Execute(IJobExecutionContext context)
{
return Task.Run(() =>
{
ApplicationLifetime.ApplicationStopping.Register(() =>
{
lock (lockHandle)
{
shouldExit = true;
}
});
try
{
for (int i = 0; i < 10; i ++)
{
lock (lockHandle)
{
if (shouldExit)
{
Logger.LogDebug($"TestJob detected that application is shutting down - exiting");
break;
}
}
Logger.LogDebug($"TestJob ran step {i+1}");
Thread.Sleep(3000);
}
}
catch (Exception exc)
{
Logger.LogError(exc, "An error occurred during execution of scheduled job");
}
});
}
}
private void ConfigureQuartz(IServiceCollection services, params Type[] jobs)
{
services.AddSingleton<IJobFactory, QuartzJobFactory>();
services.Add(jobs.Select(jobType => new ServiceDescriptor(jobType, jobType, ServiceLifetime.Singleton)));
services.AddSingleton(provider =>
{
var schedulerFactory = new StdSchedulerFactory();
var scheduler = schedulerFactory.GetScheduler().Result;
scheduler.JobFactory = provider.GetService<IJobFactory>();
scheduler.Start();
return scheduler;
});
}
protected void ConfigureJobsIoc(IServiceCollection services)
{
ConfigureQuartz(services, typeof(TestJob), /* other jobs come here */);
}
public void ConfigureServices(IServiceCollection services)
{
ConfigureJobsIoc(services);
// other stuff comes here
AddDbContext(services);
AddCors(services);
services
.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
protected void StartJobs(IApplicationBuilder app, IApplicationLifetime lifetime)
{
var scheduler = app.ApplicationServices.GetService<IScheduler>();
//TODO: use some config
QuartzServicesUtilities.StartJob<TestJob>(scheduler, TimeSpan.FromSeconds(60));
lifetime.ApplicationStarted.Register(() => scheduler.Start());
lifetime.ApplicationStopping.Register(() => scheduler.Shutdown());
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory,
ILoggingService logger, IApplicationLifetime lifetime)
{
StartJobs(app, lifetime);
// other stuff here
}