How to clean up the scheduled jobs in Episerver 10

Naming classes and methods can be difficult to do right the first time.
Once you create a scheduled job, run it and then decide to rename it, you may end up with error messages like this:

EPiServer.DataAbstraction.ScheduledJob: Failed to load type 'MyProject.ScheduledJobs.MyScheduledJob' from assembly 'MyProject' to remove this job permanent remove the contents of tblScheduledItemLog and tblScheduledItem for the job 'My Job'.

The same message can overflood your error log if you delete a scheduled job before you stop it from the admin mode.

Why this happens and what can we do about it?

To create a scheduled job, you need to decorate a class with ScheduledPlugIn attribute. On application startup, episerver will scan all assemblies and display classes that are decorated with this attribute:

episerver admin mode scheduled jobs

You can use the following code to get the same list:

PlugInDescriptor[] scheduledJobPlugins = PlugInLocator.Search(new ScheduledPlugInAttribute());
PlugInAttribute[] attributes = PlugInDescriptor.GetAttributeArray(
    scheduledJobPlugins,
    typeof(ScheduledPlugInAttribute));

var jobs = attributes.Select(x => new { x.DisplayName, x.Description }).ToList();

var result = (from attribute in attributes
                select new
                {
                    Name = attribute.DisplayName,
                    attribute.Description
                }).ToList();

episerver - getting scheduled jobs from the code

Once the scheduled job is executed or ready for execution, episerver will create an entry in the tblScheduledItem table.

If you rename the job, episerver will add a new entry in the table while keeping the old one. If you delete a job from the code, the entry remains in the table.

Since the job runner is using the tblScheduledItem table to find which job to execute next, it will report the error.

Episerver has introduced some improvements in version 10.3: http://world.episerver.com/documentation/Release-Notes/ReleaseNote/?releaseNoteId=CMS-4525

One of those improvements is the possibility to define a GUID on the ScheduledPlugIn attribute which allows you to rename the job safely. But this still doesn't solve the issue with deleted jobs.

To get a list of all scheduled jobs, the existing ones and the ones that are deleted from the code but still have some traces left in the tblScheduledItem table, you can use the IScheduledJobRepository.

var scheduledJobRepository = ServiceLocator.Current.GetInstance<IScheduledJobRepository>();
var jobs = scheduledJobRepository.List();

NOTE: While testing this, I noticed that IScheduledJobRepository sometimes doesn't return jobs that are defined in the code but never executed.

How to know which jobs are deleted from the code?

We can create a class that represents the job details:

[DebuggerDisplay("Name: {Name} | From Code: {FromCode} | Id: {Id}")]
public class JobsDetails
{
    public Guid Id { get; set; }
    public bool FromCode { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public bool IsEnabled { get; set; }
    public DateTime? LastExecution { get; set; }
}

And use the following code to get a list of all jobs (both existing ones and the ones that are deleted from the code):

public List<JobsDetails> GetScheduledJobs()
{
    var scheduledJobRepository = ServiceLocator.Current.GetInstance<IScheduledJobRepository>();

    var scheduledJobs = PlugInLocator.Search(new ScheduledPlugInAttribute());

    var scheduledJobDescriptors = PlugInDescriptor.GetAttributeArray(
        scheduledJobs,
        typeof(ScheduledPlugInAttribute));

    // ScheduledJobRepository will return jobs that have been executed at least once.
    // It may not return all jobs!
    var jobs = scheduledJobRepository.List().ToList();

    var list = (from job in jobs
                let attribute =
                scheduledJobDescriptors.FirstOrDefault(
                    x => x.PlugInType.FullName.Equals(job.TypeName, StringComparison.InvariantCultureIgnoreCase))
                select new JobsDetails
                {
                    Id = job.ID,
                    Name = job.Name,
                    IsEnabled = job.IsEnabled,
                    FromCode = attribute != null,
                    Description = attribute?.Description,
                    LastExecution = job.LastExecution
                }).ToList();

    // Scheduled jobs (classes decorated with ScheduledPlugIn attribute)
    // that are not returned by ScheduledJobRepository
    list.AddRange(from attribute in scheduledJobDescriptors
                    let existingJob =
                    jobs.FirstOrDefault(
                        x => x.TypeName.Equals(attribute.PlugInType.FullName,
                                StringComparison.InvariantCultureIgnoreCase))
                    where existingJob == null
                    select new JobsDetails
                    {
                        Name = attribute.DisplayName,
                        Description = attribute.Description,
                        FromCode = true,
                        Id = Guid.Empty,
                        IsEnabled = false,
                        LastExecution = null
                    });

    return list;
}

If we debug this, we can get something like this:

getting an overview of scheduled jobs in episerver 10

What next?

You can use this code to create your own plugin that shows an overview of all scheduled jobs. Or perhaps an initialization module that cleans the tblScheduledItem table on application startup.

IScheduledJobRepository.Delete() allows you to delete a scheduled job by ID, and JobsDetails gives you all information you need.

Who knows, maybe this will be a built-in feature in one of the upcoming versions of episerver cms.

comments powered by Disqus