Take the community feedback survey now.

Johan Björnfot
Feb 22, 2010
  7577
(0 votes)

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

continuos

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
}
Feb 22, 2010

Comments

Please login to comment.
Latest blogs
A day in the life of an Optimizely OMVP - Opticon London 2025

This installment of a day in the life of an Optimizely OMVP gives an in-depth coverage of my trip down to London to attend Opticon London 2025 held...

Graham Carr | Oct 2, 2025

Optimizely Web Experimentation Using Real-Time Segments: A Step-by-Step Guide

  Introduction Personalization has become de facto standard for any digital channel to improve the user's engagement KPI’s.  Personalization uses...

Ratish | Oct 1, 2025 |

Trigger DXP Warmup Locally to Catch Bugs & Performance Issues Early

Here’s our documentation on warmup in DXP : 🔗 https://docs.developers.optimizely.com/digital-experience-platform/docs/warming-up-sites What I didn...

dada | Sep 29, 2025

Creating Opal Tools for Stott Robots Handler

This summer, the Netcel Development team and I took part in Optimizely’s Opal Hackathon. The challenge from Optimizely was to extend Opal’s abiliti...

Mark Stott | Sep 28, 2025

Integrating Commerce Search v3 (Vertex AI) with Optimizely Configured Commerce

Introduction This blog provides a technical guide for integrating Commerce Search v3, which leverages Google Cloud's Vertex AI Search, into an...

Vaibhav | Sep 27, 2025

A day in the life of an Optimizely MVP - Opti Graph Extensions add-on v1.0.0 released

I am pleased to announce that the official v1.0.0 of the Opti Graph Extensions add-on has now been released and is generally available. Refer to my...

Graham Carr | Sep 25, 2025