This content is archived. See latest version here

Last updated: Mar 31 2014

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
{
    /// <summary>
    /// State used to keep track of position in changelog
    /// </summary>
    [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; }
    }

    /// <summary>
    /// Scheduled job that removes content asset folders where the related content has been deleted.
    /// </summary>
    [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
    {
        /// <summary>
        /// The number of items that is loaded in a batch.
        /// </summary>
        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;

        /// <summary>
        /// Initializes a new instance of the <see cref="CleanUnusedAssetsFoldersJob" /> class.
        /// </summary>
        public CleanUnusedAssetsFoldersJob()
            :this(ServiceLocator.Current.GetInstance<IContentRepository>(), ServiceLocator.Current.GetInstance<DynamicDataStoreFactory>(),
            ServiceLocator.Current.GetInstance<IChangeLog>(), ServiceLocator.Current.GetInstance<ServiceAccessor<ContentListDB>>())
        { }

        /// <summary>
        /// Initializes a new instance of the <see cref="CleanUnusedAssetsFoldersJob" /> class.
        /// </summary>
        /// <param name="contentRepository">The content repository.</param>
        /// <param name="ddsFactory">The DDS factory.</param>
        /// <param name="changeLog">The change log.</param>
        /// <param name="contentListDb">The content list db.</param>
        public CleanUnusedAssetsFoldersJob(IContentRepository contentRepository, DynamicDataStoreFactory ddsFactory, 
            IChangeLog changeLog, ServiceAccessor<ContentListDB> contentListDb)
        {
            _contentRepository = contentRepository;
            _ddsFactory = ddsFactory;
            _changeLog = changeLog;
            _contentListDb = contentListDb;
            IsStoppable = true;
        }

        /// <summary>
        /// Raise a StatusChanged event
        /// </summary>
        /// <param name="statusMessage">Status message to report</param>
        protected override void OnStatusChanged(string statusMessage)
        {
            if (_log.IsDebugEnabled)
            {
                _log.Debug(statusMessage);
            }
            base.OnStatusChanged(statusMessage);
        }

        /// <summary>
        /// Stop the job
        /// </summary>
        public override void Stop()
        {
            _stopSignaled = true;
        }

        /// <summary>
        /// Execute the job
        /// </summary>
        /// <returns></returns>
        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 have feedback on this documentation? Send an email to documentation@episerver.com. For development-related questions and discussions, refer to our Forums on https://world.episerver.com/forum/