Johan Björnfot
Feb 22, 2010
  9384
(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
Translating Optimizely CMS 12 UI components

Optimizely CMS 12 have been out for a while now, but still some elements haven't been properly translated resulting in a GUI defaulting to english....

Eric Herlitz | Jan 26, 2023 | Syndicated blog

Image preview in Optimizely CMS12 all properties view

With these simple steps, you can now see an Image and its Metadata, including size and dimensions, when editing an Image property in Optimizely...

Tomas Hensrud Gulla | Jan 26, 2023 | Syndicated blog

Setting up the ImageEditor in Optimizely CMS 12

Setting up certain configurations on Opimizely CMS 12 differs quite a bit from prior versions of (Episerver CMS 11 and older). Here's a small guide...

Eric Herlitz | Jan 25, 2023 | Syndicated blog

Happy Hour Returning in February

Hi everyone! It's been a while and we're excited to resume our Happy Hour in February for more learning, sharing, connecting, relaxing, and just to...

Patrick Lam | Jan 24, 2023

Planned breaking change for Locale parameter in Content Graph

Content Graph, a new service which makes it possible to query content using GraphQL, will have a breaking change soon. The service is currently in...

Jonas Bergqvist | Jan 23, 2023

Optimizely Advanced ContentArea Render is Back!

If you have used EPiBootstrapArea package - then back in those days we had an opportunity to render Episerver ContentArea items with different...

valdis | Jan 23, 2023 | Syndicated blog