Copied to clipboard

Flag this post as spam?

This post will be reported to the moderators as potential spam to be looked at


  • Nicholas Westby 2054 posts 7100 karma points c-trib
    Jul 18, 2014 @ 23:42
    Nicholas Westby
    0

    UmbracoContext in New Thread to Populate Cache Asynchronously

    I'm getting an ArgumentNullException when I try to do this from a new thread:

    var helper = new UmbracoHelper(UmbracoContext.Current);
    

    Basically, I'm trying to use the UmbracoHelper to access the published content so I can populate some caches in a static variable. I'd rather not hold the rendering of the page up for this, which is why I'm trying to do it in a new thread.

    Is there an easy way to set the UmbracoContext from a new thread? I tried the following, but kept running into errors, so I'm thinking there must be a better way:

    var originalHttpContext = HttpContext.Current;
    var httpContext = new HttpContextWrapper(originalHttpContext);
    var applicationContext = Umbraco.Core.ApplicationContext.Current;
    var security = new WebSecurity(httpContext, applicationContext);
    var preview = UmbracoContext.Current.InPreviewMode;
    var t = new Thread(() => {
        HttpContext.Current = originalHttpContext;
        UmbracoContext.EnsureContext(httpContext, applicationContext, security, false, preview);
        // ...Call various parts of the Umbraco API...
    });
    t.Start();
    
  • Nicholas Westby 2054 posts 7100 karma points c-trib
    Jul 21, 2014 @ 17:43
    Nicholas Westby
    0

    My workaround for anybody who is curious...

    Rather than doing the above, I'm just going to make an asynchronous request to the page with WebClient using a special query string. When that query string is present, I'll know that the page was called by code rather than by a site visitor. In that code, I can do the caching.

  • Sentient 10 posts 60 karma points
    Nov 04, 2015 @ 06:18
    Sentient
    0

    Hello Nicholas,

    Can you please post an example of your solution. I would really appreciate it as when attempting to do so myself I ran into routing, umbraco null reference exceptions, and publish issues (e.g. the virtual media folder in iis was being ignored and a default path used instead).

  • Nicholas Westby 2054 posts 7100 karma points c-trib
    Nov 04, 2015 @ 17:17
    Nicholas Westby
    0

    First, I created this class:

    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Net;
    using System.Web;
    using Umbraco.Core;
    using Umbraco.Core.Logging;
    
    namespace MyNamespace
    {
        public class TaskManager
        {
    
            #region Variables
    
            private static readonly string TaskKey = Guid.NewGuid().ToString("N");
    
            #endregion
    
            #region Properties
    
            private static object TasksLock { get; set; }
            private static Queue<Action> Tasks { get; set; }
    
            #endregion
    
            #region Constructors
    
            /// <summary>
            /// Static constructor.
            /// </summary>
            static TaskManager()
            {
                TasksLock = new object();
                Tasks = new Queue<Action>();
            }
    
            #endregion
    
            #region Public Methods
    
            /// <summary>
            /// Processes any queued tasks in an Umbraco context.
            /// </summary>
            /// <param name="request">
            /// The current HTTP request.
            /// </param>
            /// <returns>
            /// True, if this request is one intended to process requests; otherwise, false.
            /// </returns>
            /// <remarks>
            /// To avoid blocking the thread, this method will make an extra web request in a
            /// new thread and specify a query string to indicate to that request that the tasks
            /// should be processed.
            /// </remarks>
            public static bool ProcessTasksInUmbracoContext(HttpRequestBase request)
            {
    
                // Variables.
                var processMode = false;
    
                // Check for special query string.
                if (TaskKey.InvariantEquals(request.QueryString["TaskKey"]))
                {
    
                    // Process tasks.
                    processMode = true;
                    lock (TasksLock)
                    {
                        if (Tasks.Any())
                        {
                            while (Tasks.Any())
                            {
                                try
                                {
                                    var task = Tasks.Dequeue();
                                    task();
                                }
                                catch (Exception ex)
                                {
                                    LogHelper.Error<TaskManager>("Error while processing task in Umbraco context.", ex);
                                }
                            }
                        }
                    }
    
                }
                else
                {
    
                    // Call tasks from homepage (to ensure a valid Umbraco Context).
                    lock (TasksLock)
                    {
                        if (Tasks.Any())
                        {
                            var uri = request.Url;
                            var builder = new UriBuilder(uri) {Path = null, Query = "TaskKey=" + TaskKey};
                            var url = builder.Uri.AbsoluteUri;
                            ActionUtility.SafeThread(() => (new WebClient()).DownloadString(url));
                        }
                    }
    
                }
    
                // Was in process mode?
                return processMode;
    
            }
    
            /// <summary>
            /// Adds a task to the queue.
            /// </summary>
            /// <param name="action">
            /// The action to execute in an Umbraco context.
            /// </param>
            public static void AddTask(Action action)
            {
                lock (TasksLock)
                {
                    Tasks.Enqueue(action);
                }
            }
    
            #endregion
    
        }
    }
    

    Then in my main CSHTML file, I called a function on that class:

    if (TaskManager.ProcessTasksInUmbracoContext(Request))
    {
        return;
    }
    

    Anywhere I need a task to run on a different thread, I just call the AddTask function shown above to queue it up. Then, when somebody hits any page, all the tasks in the queue are processed on a different thread.

    It does that by making a request back to the site with a special query string that normal visitors don't add. When it sees that query string, it just processes the tasks.

    FYI, ActionUtility.SafeThread is just a function I created to ensure any exceptions are caught and suppressed. This is because threads that throw exceptions can cause problems for websites (i.e., IIS will restart the application pool if enough of them occur, based on whatever you have configured in IIS).

    Note that this is an ugly workaround, but it works.

  • Nicholas Westby 2054 posts 7100 karma points c-trib
    Nov 04, 2015 @ 17:21
    Nicholas Westby
    0

    For good measure, here's the implementation of SafeThread:

    /// <summary>
    /// Runs an action in a thread an ensures no exceptions escape the thread.
    /// </summary>
    /// <param name="action">The action to run in the thread.</param>
    /// <param name="errorHandler">The action to call if an exception is caught. Optional.</param>
    /// <returns>The thread (already started).</returns>
    public static Thread SafeThread(Action action, Action<Exception> errorHandler = null)
    {
        var thread = new Thread(() =>
        {
            try {
                action();
            }
            catch (ThreadAbortException ex) {
                Thread.ResetAbort();
                if (errorHandler != null) {
                    errorHandler(ex);
                }
            }
            catch (Exception ex) {
                if (errorHandler != null) {
                    errorHandler(ex);
                }
            }
            catch {
                if (errorHandler != null) {
                    errorHandler(null);
                }
            }
        });
        thread.Start();
        return thread;
    }
    
  • Sentient 10 posts 60 karma points
    Nov 05, 2015 @ 21:13
    Sentient
    0

    Thank you Nicholas. I imagine we were running into the same issues & errors as you so I will give your solution a go today. We are doing the custom setup for the benefit of a client with limited capability to speed up the backend admin processes. Something which quite a few of the clients with limited web experience have requested. (e.g. setup nodes, pre-population of fields with dynamic data, automatic creation of child nodes when the site is interacted with etc).

  • Nicholas Westby 2054 posts 7100 karma points c-trib
    Nov 05, 2015 @ 21:31
    Nicholas Westby
    0

    I had some different use cases, from the sound of it. Mine related to asynchronously handling long running tasks (e.g., extracting data from a few hundred content nodes) and doing things with published content in a new thread (e.g., when a file system watcher indicated an import file was written to the file system).

    setup nodes, pre-population of fields with dynamic data, automatic creation of child nodes when the site is interacted with etc

    If you are talking about creating child nodes when a parent node of a particular doctype is created (for example), it sounds like you want to respond to the ContentService.Saved event: https://our.umbraco.org/documentation/reference/events/contentservice-events

    From that point, you should be able to use the content service to create child nodes (and do a few other things, like pre-populate fields and such).

  • Sentient 10 posts 60 karma points
    Nov 06, 2015 @ 01:51
    Sentient
    0

    Ended up with a similar process to the above example from Nicholas but had a separate database to store queued item data to process.

    In this example a user submits event data with sub structure items in a form. What is intended is that later on the user could also batch process event data into nodes. The form data gets processed and saved to a separate db for logging and storage before attempting the insert to Umbraco (generally a set of 7+ nodes with publish event hooks etc which made the initial submission time way too long as it would be waiting for Umbraco publishing to complete). What gets returned from the query is the success state, new parent event Umbraco node id (if applicable), an error message (if applicable).

    [GET]
    public EventCreationResult ProcessEventToUmbraco(int eventPageQueueItemId)
    {
        if (eventPageQueueItemId == 0)
        {
            return new EventCreationResult 
            {
                Success = false,
                Key = "Post",
                Message = "Reference code does not correspond to an existing event."
            };
        }
        else
        {
            //get the event item by id from db or cache
            var eventItem = CacheSingletons.EventQueueItemCacheProvider.GetEventQueueItem(eventPageQueueItemId);
            if (eventItem == null || eventItem.Processed) { 
                return new EventCreationResult
                {
                    Success = false,
                    Key = "Post",
                    Message = "Reference code does not correspond to an unprocessed event."
                }; 
            }
            //set the event to have a processed flag set then perform the processing action, (note on error reset processed flag to false)
            SetEventQueueItemProcessedFlag(eventItem, true);
            //run the processing logic and return the result of the umbraco save process 
            //e.g. if one of the subnodes fails the parent gets deleted. 
            // The result is a fail & the eventPageQueueItem gets remarked as unprocessed & a user friendly message is returned
            // if the event is processed successfully into umbraco the result is a success, the processed datetime will be set and the umbraco id returned
            var result = ProcessEventNodesToUmbraco(eventItem, HttpContext);              
            return result; 
        }
    }
    

    So on form submit for a single event once validated and saved to the db the remote call to method above is sent

    string baseUrl = Request.Url.Scheme + "://" + Request.Url.Authority + Request.ApplicationPath.TrimEnd('/') + "/";
    var processRequest = (HttpWebRequest)WebRequest.Create(new System.Uri(
        baseUrl + "ProcessEventToUmbraco?eventPageQueueItemId=" +
        result.EventPageQueueItem.EventPageQueueItemId).AbsoluteUri);
    
    processRequest.Method = "GET";
    Task.Factory.StartNew(() =>
    {
        processRequest.GetResponse();
    });
    

    Given that the above uses a database to act as a intermediary stage for information the runtime of the parent thread is longer. (Db crud time). But otherwise allows the admin user to remotely call the action if needed by id (or other properties). Ideally the action would be within a controller that checks for authorised backoffice users.

    The setup for a TaskManager is a good idea and I am testing using that for the batch processing of unprocessed event items. This includes the use of locking objects etc to prevent the same call being run simultaneously.
    At the moment the method data for reference is being passed through the query string. Setting it up to submit the full event object & reduce the db retrieve calls would not be to much more difficult.

Please Sign in or register to post replies

Write your reply to:

Draft