Johan Björnfot
Feb 22, 2010
  9961
(1 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
Opti ID overview

Opti ID allows you to log in once and switch between Optimizely products using Okta, Entra ID, or a local account. You can also manage all your use...

K Khan | Jul 26, 2024

Getting Started with Optimizely SaaS using Next.js Starter App - Extend a component - Part 3

This is the final part of our Optimizely SaaS CMS proof-of-concept (POC) blog series. In this post, we'll dive into extending a component within th...

Raghavendra Murthy | Jul 23, 2024 | Syndicated blog

Optimizely Graph – Faceting with Geta Categories

Overview As Optimizely Graph (and Content Cloud SaaS) makes its global debut, it is known that there are going to be some bugs and quirks. One of t...

Eric Markson | Jul 22, 2024 | Syndicated blog

Integration Bynder (DAM) with Optimizely

Bynder is a comprehensive digital asset management (DAM) platform that enables businesses to efficiently manage, store, organize, and share their...

Sanjay Kumar | Jul 22, 2024

Frontend Hosting for SaaS CMS Solutions

Introduction Now that CMS SaaS Core has gone into general availability, it is a good time to start discussing where to host the head. SaaS Core is...

Minesh Shah (Netcel) | Jul 20, 2024

Optimizely London Dev Meetup 11th July 2024

On 11th July 2024 in London Niteco and Netcel along with Optimizely ran the London Developer meetup. There was an great agenda of talks that we put...

Scott Reed | Jul 19, 2024