Johan Björnfot
Feb 22, 2010
  10028
(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
I'm running Optimizely CMS on .NET 9!

It works 🎉

Tomas Hensrud Gulla | Nov 12, 2024 | Syndicated blog

Recraft's image generation with AI-Assistant for Optimizely

Recraft V3 model is outperforming all other models in the image generation space and we are happy to share: Recraft's new model is now available fo...

Luc Gosso (MVP) | Nov 8, 2024 | Syndicated blog

ExcludeDeleted(): Prevent Trashed Content from Appearing in Search Results

Introduction In Optimizely CMS, content that is moved to the trash can still appear in search results if it’s not explicitly excluded using the...

Ashish Rasal | Nov 7, 2024

CMS + CMP + Graph integration

We have just released a new package https://nuget.optimizely.com/package/?id=EPiServer.Cms.WelcomeIntegration.Graph which changes the way CMS fetch...

Bartosz Sekula | Nov 5, 2024