An Example of Implementing Web Timing Task on AspNet Core

Time:2019-10-9

As a back-end program dog, project practice often encounters the work of timed tasks. The most easy way to think of is to deploy timed task programs/scripts on the host using host methods such as Windows Scheduled Tasks/wndows Service Programs/Crontab Programs.

But most of the time, if you use a shared host or a controlled host, these hosts do not allow you to install exe programs or Windows service programs privately.

The code armor will think of timing tasks in Web applications. There are two directions at present:

  • AspNetCore’s Host Service, which is a lightweight back-end service, needs to be matched with timer to complete the timing task.
  • Second, the old Quartz.Net component supports complex and flexible Scheduling, ADO/RAM Job task storage, cluster, monitor and plug-in.

Here our project uses a slightly more sophisticated Quartz. net for web timing tasks.

Project Background

Recently, we need to do a counting program: using redis counting to set up an hourly persistence of the accumulated data of that day to the relational database sqlite.

Add Quartz.Net Nuget dependency packages: <PackageReference Include=”Quartz” Version=”3.0.6″/>

  • Define the content of timed tasks: Job
  • (2) Setting trigger conditions: Trigger
  • (3) Integrating Quartz.Net into AspNet Core

Brainstorming

The IScheduler class wraps the work of point 1 and point 2 that needs to be done in the above context. SimpleJobFactory defines the process of generating a specified Job task, which is a Job instance constructed by invoking a parametric constructor using a reflection mechanism. The following is the source code:

// From the Quartz. Simpl. SimpleJobFactory class
using System;
using Quartz.Logging;
using Quartz.Spi;
using Quartz.Util;
namespace Quartz.Simpl
{
 /// <summary> 
 /// The default JobFactory used by Quartz - simply calls 
 /// <see cref="ObjectUtils.InstantiateType{T}" /> on the job class.
 /// </summary>
 /// <seealso cref="IJobFactory" />
 /// <seealso cref="PropertySettingJobFactory" />
 /// <author>James House</author>
 /// <author>Marko Lahma (.NET)</author>
 public class SimpleJobFactory : IJobFactory
 {
  private static readonly ILog log = LogProvider.GetLogger(typeof (SimpleJobFactory));

  /// <summary>
  /// Called by the scheduler at the time of the trigger firing, in order to
  /// produce a <see cref="IJob" /> instance on which to call Execute.
  /// </summary>
  /// <remarks>
  /// It should be extremely rare for this method to throw an exception -
  /// basically only the case where there is no way at all to instantiate
  /// and prepare the Job for execution. When the exception is thrown, the
  /// Scheduler will move all triggers associated with the Job into the
  /// <see cref="TriggerState.Error" /> state, which will require human
  /// intervention (e.g. an application restart after fixing whatever
  /// configuration problem led to the issue with instantiating the Job).
  /// </remarks>
  /// <param name="bundle">The TriggerFiredBundle from which the <see cref="IJobDetail" />
  /// and other info relating to the trigger firing can be obtained.</param>
  /// <param name="scheduler"></param>
  /// <returns>the newly instantiated Job</returns>
  /// <throws> SchedulerException if there is a problem instantiating the Job. </throws>
  public virtual IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
  {
   IJobDetail jobDetail = bundle.JobDetail;
   Type jobType = jobDetail.JobType;
   try
   {
    if (log.IsDebugEnabled())
    {
     log.Debug($"Producing instance of Job '{jobDetail.Key}', class={jobType.FullName}");
    }

    return ObjectUtils.InstantiateType<IJob>(jobType);
   }
   catch (Exception e)
   {
    SchedulerException se = new SchedulerException($"Problem instantiating class '{jobDetail.JobType.FullName}'", e);
    throw se;
   }
  }

  /// <summary>
  /// Allows the job factory to destroy/cleanup the job if needed. 
  /// No-op when using SimpleJobFactory.
  /// </summary>
  public virtual void ReturnJob(IJob job)
  {
   var disposable = job as IDisposable;
   disposable?.Dispose();
  }
 }
}

// From the Quartz. Util. ObjectUtils class - -----------------------------------------------------------------------------------------------------------------------------------------
 public static T InstantiateType<T>(Type type)
{
  if (type == null)
  {
   throw new ArgumentNullException(nameof(type), "Cannot instantiate null");
  }
  ConstructorInfo ci = type.GetConstructor(Type.EmptyTypes);
  if (ci == null)
  {
   throw new ArgumentException("Cannot instantiate type which has no empty constructor", type.Name);
  }
  return (T) ci.Invoke(new object[0]);
}

In many cases, the Job task defined depends on other components, and the default SimpleJobFactory is not available. We need to consider adding Job task as a dependency injection component to the dependency injection container.

Key ideas:

(1) IScheduler opens the JobFactory property to facilitate you to control the instantiation of Job tasks;

JobFactories may be of use to those wishing to have their application produce IJob instances via some special mechanism, such as to give the opportunity for dependency injection
(2) The service architecture of AspNet Core is based on dependency injection, which uses the existing dependency injection container IServiceProvider of AspNet Core to manage the creation process of Job services.

Coding practice

Define Job content:

// To persist redis data to SQLite every hour, jump needles every morning, and persist yesterday's data all day.
public class UsageCounterSyncJob : IJob
{
  private readonly EqidDbContext _context;
  private readonly IDatabase _redisDB1;
  private readonly ILogger _logger;
  public UsageCounterSyncJob(EqidDbContext context, RedisDatabase redisCache, ILoggerFactory loggerFactory)
  {
   _context = context;
   _redisDB1 = redisCache[1];
   _logger = loggerFactory.CreateLogger<UsageCounterSyncJob>();
  }
   public async Task Execute(IJobExecutionContext context)
  {
   // The trigger time is in the early morning, synchronizing yesterday's count
   var _day = DateTime.Now.ToString("yyyyMMdd");
   if (context.FireTimeUtc.LocalDateTime.Hour == 0)
    _day = DateTime.Now.AddDays(-1).ToString("yyyyMMdd");

   await SyncRedisCounter(_day);
   _logger.LogInformation("[UsageCounterSyncJob] Schedule job executed.");
  }
  ......
 }

(2) Register Job and Trigger:

namespace EqidManager
{
 using IOCContainer = IServiceProvider;
 // Register job and trigger after Quartz.Net starts
 public class QuartzStartup
 {
  public IScheduler _scheduler { get; set; }

  private readonly ILogger _logger;
  private readonly IJobFactory iocJobfactory;
  public QuartzStartup(IOCContainer IocContainer, ILoggerFactory loggerFactory)
  {
   _logger = loggerFactory.CreateLogger<QuartzStartup>();
   iocJobfactory = new IOCJobFactory(IocContainer);
   var schedulerFactory = new StdSchedulerFactory();
   _scheduler = schedulerFactory.GetScheduler().Result;
   _scheduler.JobFactory = iocJobfactory;
  }

  public void Start()
  {
   _logger.LogInformation("Schedule job load as application start.");
   _scheduler.Start().Wait();

   var UsageCounterSyncJob = JobBuilder.Create<UsageCounterSyncJob>()
    .WithIdentity("UsageCounterSyncJob")
    .Build();

   var UsageCounterSyncJobTrigger = TriggerBuilder.Create()
    .WithIdentity("UsageCounterSyncCron")
    .StartNow()
    // Synchronize every hour
    .WithCronSchedule("0 0 * * * ?")  // Seconds,Minutes,Hours,Day-of-Month,Month,Day-of-Week,Year(optional field)
    .Build();
   _scheduler.ScheduleJob(UsageCounterSyncJob, UsageCounterSyncJobTrigger).Wait();

   _scheduler.TriggerJob(new JobKey("UsageCounterSyncJob"));
  }

  public void Stop()
  {
   if (_scheduler == null)
   {
    return;
   }

   if (_scheduler.Shutdown(waitForJobsToComplete: true).Wait(30000))
    _scheduler = null;
   else
   {
   }
   _logger.LogCritical("Schedule job upload as application stopped");
  }
 }

 /// <summary>
 /// IOCJobFactory: Implement injection to generate corresponding Job components when Timer triggers
 /// </summary>
 public class IOCJobFactory : IJobFactory
 {
  protected readonly IOCContainer Container;

  public IOCJobFactory(IOCContainer container)
  {
   Container = container;
  }

  //Called by the scheduler at the time of the trigger firing, in order to produce
  //  a Quartz.IJob instance on which to call Execute.
  public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
  {
   return Container.GetService(bundle.JobDetail.JobType) as IJob;
  }

  // Allows the job factory to destroy/cleanup the job if needed.
  public void ReturnJob(IJob job)
  {
  }
 }
}

(3) Injection component combined with ASpNet Core; Binding Quartz.Net

// --------------------------------------------------------------------------------------------------------------------------------------------------------------------------
......
Services. AddTransient < UsageCounterSyncJob >(); // Instantaneous dependency injection is used here
services.AddSingleton<QuartzStartup>();
......

// Binding Quartz.Net
public void Configure(IApplicationBuilder app, Microsoft.AspNetCore.Hosting.IApplicationLifetime lifetime, ILoggerFactory loggerFactory)
{
  var quartz = app.ApplicationServices.GetRequiredService<QuartzStartup>();
  lifetime.ApplicationStarted.Register(quartz.Start);
  lifetime.ApplicationStopped.Register(quartz.Stop);
}

Attachment: Solutions to Low Frequency Access of IIS Website Causing Work Processes to Stay idle

IIS sets a default 20-minute idle timeout for the website: no requests are processed or new requests are received within 20 minutes, and the working process is idle.

Low-frequency web access on IIS will cause the workflow process to shut down, at which time application pool recycling, thread resources such as Timer will be destroyed; Timer may be regenerated when the workflow re-operates, but our set timing Job may not be correctly executed on demand.

Therefore, in order to achieve the timing task of low-frequency web access in IIS website:

Idle TimeOut = 0 is set; at the same time, [application pool] – > [recycling in progress] – > uncheck [recycling condition]