Continuous mirroring – CMS 6
Normally a mirroring job is started from a mirroring scheduled job. The only thing however the scheduled job does is to make a request to the Mirroring Service that it should start the mirroring. The actual work is performed by the mirroring service which will check in the changelog for changes that have been done since the last time the mirroring job executed. The changed files and pages will then be mirrored to the destination.
The idea with the sample code in this post is to start a mirroring job directly when an event occurs instead of waiting for the next time the scheduled job is executed.
In the code below I have written a module that listens to page and file related events that can affect configured mirroring channels (a channel is a configured mirroring job). When an event occurs it does a check if the change might affect the channel and if so makes a request to the mirroring service to start a mirroring job for that channel. It would be possible to skip the event check that is done in the module and always send a StartMirroring request to the service (the mirroring service will anyway get relevant changes from the changelog). However to avoid unnecessary calls to the service I added some checks before calling the service.
One thing to be aware of is that it is only possible to run one job at the time for each channel. The StartMirroring call returns a boolean, true means the job was started and false means a job is already executing for that channel. In my implementation I ignore the response, the consequence is that some changes will not be mirrored immediately (in case a previous job has not finished yet). However no changes are lost, they will be mirrored the next time the job is executed. An alternative (if you really want all changes to be done as soon as possible) is to handle the case where StartMirroring returns false and for example retry after some time.
Configuration
In my code I use the Parameters field in the mirroring configuration to configure if a channel should be continuously mirrored or not. See picture below
Then at start up of the application all channels are checked to see which are configured to run continuously. One drawback with my implementation is that you have to restart the application before changes to a mirroring channel is applied. Another option is to have the configuration in a configuration file instead.
[InitializableModule] public class ContinuousMirroring : IInitializableModule { internal class ChannelInfo { internal Guid ChannelId { get; set; } internal String SourceEndpoint { get; set; } internal String SiteId { get; set; } } internal enum EventType { PageDeleteOrMove, PageChange, FileChange } // no locking needed, only filled at startup then never changed private Dictionary<PageReference, ChannelInfo> _mirroringChannels = new Dictionary<PageReference, ChannelInfo>(); #region IInitializableModule Members public void Initialize(EPiServer.Framework.Initialization.InitializationEngine context) { context.InitComplete += delegate { //Find Mirroring Channels that are set to continuous foreach (MirroringData channel in MirroringData.ListEnabled().Where(channel => channel.Params.Contains("continuos"))) { RegisterChannel(channel); } if (_mirroringChannels.Count > 0) { DataFactory.Instance.CreatedPage += new PageEventHandler(PageEvent); DataFactory.Instance.DeletedPage += new PageEventHandler(PageMovedOrDeleted); DataFactory.Instance.DeletedPageLanguage += new PageEventHandler(PageEvent); DataFactory.Instance.MovedPage += new PageEventHandler(PageMovedOrDeleted); DataFactory.Instance.PublishedPage += new PageEventHandler(PageEvent); UnifiedFile.UnifiedFileChanged += new UnifiedFileStreamEventHandler(FileChanged); UnifiedFile.UnifiedFileCheckedIn += new UnifiedFileEventHandler(FileChanged); } }; } void RegisterChannel(MirroringData channelData) { PermanentPageLinkMap pageLinkMap = PermanentLinkMapStore.Find(channelData.FromPageGuid) as PermanentPageLinkMap; if (pageLinkMap != null) { string siteId = EPiServer.MirroringService.Common.ConfigurationHandler.GetSiteIdForPage(pageLinkMap.PageReference); ChannelInfo channelInfo = (from siteElement in EPiServerSection.Instance.Sites.Cast<SiteElement>() where String.Equals(siteId, siteElement.SiteId, StringComparison.OrdinalIgnoreCase) select new ChannelInfo() { ChannelId = channelData.Id.ExternalId, SiteId = siteId, SourceEndpoint = siteElement.SiteSettings.MirroringSource }).SingleOrDefault(); if (channelInfo != null) { _mirroringChannels[pageLinkMap.PageReference] = channelInfo; } } } void PageEvent(object sender, PageEventArgs e) { ThreadPool.QueueUserWorkItem(new WaitCallback(EventNotifier), new object[] { EventType.PageChange, e }); } void PageMovedOrDeleted(object sender, PageEventArgs e) { ThreadPool.QueueUserWorkItem(new WaitCallback(EventNotifier), new object[] { EventType.PageDeleteOrMove, e }); } void FileChanged(object sender, UnifiedVirtualPathEventArgs e) { ThreadPool.QueueUserWorkItem(new WaitCallback(EventNotifier), new object[] { EventType.FileChange, e }); } private void EventNotifier(object data) { object[] eventData = (object[])data; EventType eventType = (EventType)eventData[0]; switch (eventType) { case EventType.FileChange: HandleFileChange((UnifiedVirtualPathEventArgs)eventData[1]); break; case EventType.PageChange: HandlePageChange((PageEventArgs)eventData[1]); break; default: //EventType.PageDeleteOrMove: HandlePageMoveOrDelete((PageEventArgs)eventData[1]); break; } } private void HandlePageChange(PageEventArgs e) { foreach (ChannelInfo channel in _mirroringChannels.Where(entry => IsBelowChannelRoot(entry.Key, e)).Select(entry => entry.Value)) { MirroringSourceClient mirroringSourceClient = new MirroringSourceClient(channel.SourceEndpoint); mirroringSourceClient.StartMirroring(channel.SiteId, channel.ChannelId, ValidationContext.MirroringWithOutValidation); } } private void HandlePageMoveOrDelete(PageEventArgs e) { //From page event args we cant decide which channel that might be effected. //Send job for all channels to Mirroring Service and let it decide the action foreach (ChannelInfo channel in _mirroringChannels.Values) { MirroringSourceClient mirroringSourceClient = new MirroringSourceClient(channel.SourceEndpoint); mirroringSourceClient.StartMirroring(channel.SiteId, channel.ChannelId, ValidationContext.MirroringWithOutValidation); } } private void HandleFileChange(UnifiedVirtualPathEventArgs e) { //Use SoftLink to find all pages that reference the file foreach (SoftLink link in SoftLink.Load(e.VirtualPath ?? e.NewVirtualPath).Where(softLink => !PageReference.IsNullOrEmpty(softLink.OwnerPageLink))) { foreach (ChannelInfo channel in _mirroringChannels.Where(entry => IsAncestor(entry.Key, link.OwnerPageLink)).Select(entry => entry.Value)) { MirroringSourceClient mirroringSourceClient = new MirroringSourceClient(channel.SourceEndpoint); mirroringSourceClient.StartMirroring(channel.SiteId, channel.ChannelId, ValidationContext.MirroringWithOutValidation); } } } private bool IsBelowChannelRoot(PageReference pageReference, PageEventArgs e) { if (!PageReference.IsNullOrEmpty(e.PageLink) && IsAncestor(pageReference, e.PageLink)) { return true; } else if (!PageReference.IsNullOrEmpty(e.TargetLink) && IsAncestor(pageReference, e.TargetLink)) { return true; } return false; } private bool IsAncestor(PageReference channelRoot, PageReference pageLink) { PageReference pageLinkWithoutVersion = new PageReference(pageLink.ID, 0, pageLink.RemoteSite); while (pageLinkWithoutVersion != channelRoot && pageLinkWithoutVersion != PageReference.RootPage) { pageLinkWithoutVersion = EPiServer.DataFactory.Instance.GetPage(pageLinkWithoutVersion).ParentLink; } return pageLinkWithoutVersion == channelRoot; } public void Uninitialize(EPiServer.Framework.Initialization.InitializationEngine context) { if (_mirroringChannels.Count > 0) { DataFactory.Instance.CreatedPage -= new PageEventHandler(PageEvent); DataFactory.Instance.DeletedPage -= new PageEventHandler(PageMovedOrDeleted); DataFactory.Instance.DeletedPageLanguage -= new PageEventHandler(PageEvent); DataFactory.Instance.MovedPage -= new PageEventHandler(PageMovedOrDeleted); DataFactory.Instance.PublishedPage -= new PageEventHandler(PageEvent); UnifiedFile.UnifiedFileChanged -= new UnifiedFileStreamEventHandler(FileChanged); UnifiedFile.UnifiedFileCheckedIn -= new UnifiedFileEventHandler(FileChanged); } } public bool IsInitialized {get;set;} public void Preload(string[] parameters){} #endregion }
Comments