Introduction
This document provides an introduction to scheduled jobs in EPiServer CMS. Scheduled jobs will run in the background with preset time intervals and typically do cleanup and updatings tasks. A sample installation of EPiServer CMS comes with a number of predefined scheduled jobs which are administered in the admin view. These, together with the administration of scheduled jobs, are described in detail in the Administration part of the EPiServer CMS User Guide.
Some of these scheduled jobs can be customized and further configured. It is also possible to add your own scheduled jobs, for instance for the automatic updating of the geographic location database for the personalization features included with EPiServer CMS.
Execution
During initialization of EPiServer CMS the system will scan through all jobs and check for their next execution time. Then at execution time the job will be called for execution. It is also possible to execute a job manually from admin mode. Since scheduled jobs are executed on the site a requirement for the job to be executed is that the site is up and running. This can be done for example by using IIS feature "Application Initialization" or having a web site supervisor that periodically pings the site.
In case there are several sites sharing the same database, for example when having a loadbalanced scenario it is possible to control which site that should execute scheduled jobs. This is achieved by setting attribute enableScheduler on configuration element /episerver/sites/site/siteSetting to true on the site that should execute the jobs and false on the other sites. In case several sites are configured to run scheduled jobs then each job will be scheduled for execution on all sites. But during execution the first site that starts executing a specific job will mark it in database as executing and then the other sites will not execute that job, hence a job will not run in parallell on several sites.
Implementing a scheduled job
To implement a scheduled job you need to have a class marked with ScheduledPlugInAttribute. The recommendation is to inherit base class EPiServer.BaseLibrary.Scheduling.JobBase (if the class does not inherit the baseclass it needs to have a static method named "Execute" without parameters that return a string. Below is the code for one of the built in scheduled jobs that deletes unused content resources.
C#
using EPiServer.BaseLibrary.Scheduling;
using EPiServer.ChangeLog;
using EPiServer.Core;
using EPiServer.Data.Dynamic;
using EPiServer.DataAbstraction;
using EPiServer.DataAccess;
using EPiServer.PlugIn;
using EPiServer.Security;
using EPiServer.ServiceLocation;
using EPiServer.Web;
using log4net;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
namespace EPiServer.Util
{
[EPiServerDataTable(TableName = StoreDefinitionParameters.SystemStorageTableName)]
public class ContentAssetsCleanupJobState : IDynamicData
{
public static Guid SingletonId = new Guid("FE65E881-34C8-439D-AC82-066ABF532EA6");
public Data.Identity Id
{
get;
set;
}
public long LastSequenceNumber { get; set; }
}
[ScheduledPlugIn(DisplayName = "Remove Unrelated Content Resources", LanguagePath = "/admin/databasejob/contentassetscleanupjob", HelpFile = "contentassetscleanupjob", DefaultEnabled = true, InitialTime = "1.1:0:0", IntervalLength = 1, IntervalType = ScheduledIntervalType.Weeks)]
public class CleanUnusedAssetsFoldersJob : JobBase
{
public const int BatchSize = 10;
private bool _stopSignaled;
private IContentRepository _contentRepository;
private DynamicDataStoreFactory _ddsFactory;
private static ILog _log = LogManager.GetLogger(typeof(CleanUnusedAssetsFoldersJob));
private IChangeLog _changeLog;
private ServiceAccessor<ContentListDB> _contentListDb;
public CleanUnusedAssetsFoldersJob()
:this(ServiceLocator.Current.GetInstance<IContentRepository>(), ServiceLocator.Current.GetInstance<DynamicDataStoreFactory>(),
ServiceLocator.Current.GetInstance<IChangeLog>(), ServiceLocator.Current.GetInstance<ServiceAccessor<ContentListDB>>())
{ }
public CleanUnusedAssetsFoldersJob(IContentRepository contentRepository, DynamicDataStoreFactory ddsFactory,
IChangeLog changeLog, ServiceAccessor<ContentListDB> contentListDb)
{
_contentRepository = contentRepository;
_ddsFactory = ddsFactory;
_changeLog = changeLog;
_contentListDb = contentListDb;
IsStoppable = true;
}
protected override void OnStatusChanged(string statusMessage)
{
if (_log.IsDebugEnabled)
{
_log.Debug(statusMessage);
}
base.OnStatusChanged(statusMessage);
}
public override void Stop()
{
_stopSignaled = true;
}
public override string Execute()
{
int numberOfDeletedFolders = 0;
var logQuery = new ChangeLogQueryInfo();
logQuery.MaxRecordsToReturn = BatchSize;
logQuery.Category = (int)ChangeLogCategory.Content;
logQuery.Action = (int)ChangeLogContent.ActionType.DeletedItems;
logQuery.StartSequenceNumber = LoadLastSequenceNumber() + 1;
OnStatusChanged("Starting reading deleted content from change log");
IList<IChangeLogItem> changes;
do
{
changes = _changeLog.GetChanges(logQuery, ReadDirection.Forwards, ChangeLog.SortOrder.Ascending);
if (changes == null || changes.Count == 0)
{
break;
}
for (int i = 0; i < changes.Count; i++)
{
var deletedItems = (ChangeLogContentDeletedItems)changes[i];
var deletedReferences =_contentListDb().ListOwnedContentAssetReferences(deletedItems.DeletedIdentities);
foreach (var assetFolderReference in deletedReferences)
{
_contentRepository.Delete(assetFolderReference, false, AccessLevel.NoAccess);
numberOfDeletedFolders++;
if (_stopSignaled)
{
return "Stop of job was called. Number of deleted asset folders: " + numberOfDeletedFolders;
}
}
}
SaveLastSequenceNumber(changes[changes.Count - 1].SequenceNumber);
OnStatusChanged(String.Format(CultureInfo.InvariantCulture, "Deleted '{0}' content asset folders", numberOfDeletedFolders));
}
while (changes.Count > 0);
return String.Format(CultureInfo.InvariantCulture, "'{0}' unused content asset folders have been deleted", numberOfDeletedFolders);
}
internal virtual long LoadLastSequenceNumber()
{
using (DynamicDataStore store = _ddsFactory.GetStore(typeof(ContentAssetsCleanupJobState)) ?? _ddsFactory.CreateStore(typeof(ContentAssetsCleanupJobState)))
{
var state = store.Load<ContentAssetsCleanupJobState>(ContentAssetsCleanupJobState.SingletonId);
return state == null ? 0 : state.LastSequenceNumber;
}
}
internal virtual void SaveLastSequenceNumber(long lastSequenceNumber)
{
using (DynamicDataStore store = _ddsFactory.GetStore(typeof(ContentAssetsCleanupJobState)) ?? _ddsFactory.CreateStore(typeof(ContentAssetsCleanupJobState)))
{
var state = new ContentAssetsCleanupJobState();
state.Id = ContentAssetsCleanupJobState.SingletonId;
state.LastSequenceNumber = lastSequenceNumber;
store.Save(state);
}
}
}
}
Do you find this information helpful? Please log in to provide feedback.