From 42ef6d3d76fa2b7972baea07204e5f250fc62518 Mon Sep 17 00:00:00 2001 From: Benjamin Ketron Date: Tue, 28 Jun 2016 12:03:36 -0600 Subject: [PATCH] An attempt to speed up sorting a large set of nodes when a large set of users exist. Our sorts were failing in azure because the user notifications were taking long enough that azure would kill the thread. --- .../Services/INotificationService.cs | 14 +++ .../Services/NotificationService.cs | 89 ++++++++++++++++++- .../NotificationServiceExtensions.cs | 64 +++++++++++++ .../Strategies/NotificationsHandler.cs | 12 ++- .../umbraco/webservices/nodeSorter.asmx.cs | 14 +-- 5 files changed, 181 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Core/Services/INotificationService.cs b/src/Umbraco.Core/Services/INotificationService.cs index c067b2d615..bfb9938e46 100644 --- a/src/Umbraco.Core/Services/INotificationService.cs +++ b/src/Umbraco.Core/Services/INotificationService.cs @@ -29,6 +29,20 @@ namespace Umbraco.Core.Services Func createSubject, Func createBody); + /// + /// Sends the notifications for the specified user regarding the specified node and action. + /// + /// + /// + /// + /// + /// + /// + /// + void SendNotifications(IUser operatingUser, IEnumerable entities, string action, string actionName, HttpContextBase http, + Func createSubject, + Func createBody); + /// /// Gets the notifications for the user /// diff --git a/src/Umbraco.Core/Services/NotificationService.cs b/src/Umbraco.Core/Services/NotificationService.cs index 41e2ff698e..18ad945122 100644 --- a/src/Umbraco.Core/Services/NotificationService.cs +++ b/src/Umbraco.Core/Services/NotificationService.cs @@ -96,6 +96,80 @@ namespace Umbraco.Core.Services } } + /// + /// Sends the notifications for the specified user regarding the specified node and action. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// Currently this will only work for Content entities! + /// + public void SendNotifications(IUser operatingUser, IEnumerable entities, string action, string actionName, HttpContextBase http, + Func createSubject, + Func createBody) + { + if ((entities is IEnumerable) == false) + { + throw new NotSupportedException(); + } + + //we'll lazily get these if we need to send notifications + Dictionary> allVersionsDictionary = new Dictionary>(); + + int totalUsers; + var allUsers = _userService.GetAll(0, int.MaxValue, out totalUsers); + foreach (var u in allUsers) + { + if (u.IsApproved == false) continue; + var userNotifications = GetUserNotifications(u).ToArray(); + + foreach (var entity in entities) + { + var content = (IContent) entity; + + var userNotificationsByPath = FilterUserNotificationsByPath(userNotifications, content.Path); + var notificationForAction = userNotificationsByPath + .FirstOrDefault(x => x.Action == action); + + if (notificationForAction != null) + { + IEnumerable allVersions = null; + if (allVersionsDictionary.ContainsKey(entity.Id)) + { + allVersions = allVersionsDictionary[entity.Id]; + } + + //lazy load versions if notifications are required + if (allVersions == null) + { + allVersions = _contentService.GetVersions(entity.Id); + allVersionsDictionary[entity.Id] = allVersions; + } + + try + { + SendNotification( + operatingUser, u, content, + allVersions, + actionName, http, createSubject, createBody); + + _logger.Debug(string.Format("Notification type: {0} sent to {1} ({2})", action, u.Name, u.Email)); + } + catch (Exception ex) + { + _logger.Error("An error occurred sending notification", ex); + } + } + } + } + } + + /// /// Gets the notifications for the user /// @@ -120,11 +194,22 @@ namespace Umbraco.Core.Services public IEnumerable GetUserNotifications(IUser user, string path) { var userNotifications = GetUserNotifications(user).ToArray(); - var pathParts = path.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries); - var result = userNotifications.Where(r => pathParts.InvariantContains(r.EntityId.ToString(CultureInfo.InvariantCulture))).ToList(); + var result = FilterUserNotificationsByPath(userNotifications, path); return result; } + /// + /// Filters a userNotifications collection by a path + /// + /// + /// + /// + public IEnumerable FilterUserNotificationsByPath(IEnumerable userNotifications, string path) { + var pathParts = path.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries); + var result = userNotifications.Where(r => pathParts.InvariantContains(r.EntityId.ToString(CultureInfo.InvariantCulture))).ToList(); + return result; + } + /// /// Deletes notifications by entity /// diff --git a/src/Umbraco.Web/NotificationServiceExtensions.cs b/src/Umbraco.Web/NotificationServiceExtensions.cs index 7beff605f9..b6182590c8 100644 --- a/src/Umbraco.Web/NotificationServiceExtensions.cs +++ b/src/Umbraco.Web/NotificationServiceExtensions.cs @@ -9,6 +9,7 @@ using Umbraco.Core.Services; using umbraco; using umbraco.BusinessLogic.Actions; using umbraco.interfaces; +using System.Collections.Generic; namespace Umbraco.Web { @@ -27,6 +28,16 @@ namespace Umbraco.Web service.SendNotification(entity, action, UmbracoContext.Current); } + internal static void SendNotification(this INotificationService service, IEnumerable entities, IAction action, ApplicationContext applicationContext) + { + if (UmbracoContext.Current == null) + { + LogHelper.Warn(typeof(NotificationServiceExtensions), "Cannot send notifications, there is no current UmbracoContext"); + return; + } + service.SendNotification(entities, action, UmbracoContext.Current); + } + internal static void SendNotification(this INotificationService service, IUmbracoEntity entity, IAction action, UmbracoContext umbracoContext) { if (umbracoContext == null) @@ -37,6 +48,16 @@ namespace Umbraco.Web service.SendNotification(entity, action, umbracoContext, umbracoContext.Application); } + internal static void SendNotification(this INotificationService service, IEnumerable entities, IAction action, UmbracoContext umbracoContext) + { + if (umbracoContext == null) + { + LogHelper.Warn(typeof(NotificationServiceExtensions), "Cannot send notifications, there is no current UmbracoContext"); + return; + } + service.SendNotification(entities, action, umbracoContext, umbracoContext.Application); + } + internal static void SendNotification(this INotificationService service, IUmbracoEntity entity, IAction action, UmbracoContext umbracoContext, ApplicationContext applicationContext) { if (umbracoContext == null) @@ -61,6 +82,30 @@ namespace Umbraco.Web service.SendNotification(user, entity, action, umbracoContext, applicationContext); } + internal static void SendNotification(this INotificationService service, IEnumerable entities, IAction action, UmbracoContext umbracoContext, ApplicationContext applicationContext) + { + if (umbracoContext == null) + { + LogHelper.Warn(typeof(NotificationServiceExtensions), "Cannot send notifications, there is no current UmbracoContext"); + return; + } + + var user = umbracoContext.Security.CurrentUser; + + //if there is no current user, then use the admin + if (user == null) + { + LogHelper.Debug(typeof(NotificationServiceExtensions), "There is no current Umbraco user logged in, the notifications will be sent from the administrator"); + user = applicationContext.Services.UserService.GetUserById(0); + if (user == null) + { + LogHelper.Warn(typeof(NotificationServiceExtensions), "Noticiations can not be sent, no admin user with id 0 could be resolved"); + return; + } + } + service.SendNotification(user, entities, action, umbracoContext, applicationContext); + } + internal static void SendNotification(this INotificationService service, IUser sender, IUmbracoEntity entity, IAction action, UmbracoContext umbracoContext, ApplicationContext applicationContext) { if (sender == null) throw new ArgumentNullException("sender"); @@ -79,5 +124,24 @@ namespace Umbraco.Web ? ui.Text("notifications", "mailBody", strings, mailingUser) : ui.Text("notifications", "mailBodyHtml", strings, mailingUser)); } + + internal static void SendNotification(this INotificationService service, IUser sender, IEnumerable entities, IAction action, UmbracoContext umbracoContext, ApplicationContext applicationContext) + { + if (sender == null) throw new ArgumentNullException("sender"); + if (umbracoContext == null) throw new ArgumentNullException("umbracoContext"); + if (applicationContext == null) throw new ArgumentNullException("applicationContext"); + + + applicationContext.Services.NotificationService.SendNotifications( + sender, + entities, + action.Letter.ToString(CultureInfo.InvariantCulture), + ui.Text("actions", action.Alias), + umbracoContext.HttpContext, + (mailingUser, strings) => ui.Text("notifications", "mailSubject", strings, mailingUser), + (mailingUser, strings) => UmbracoConfig.For.UmbracoSettings().Content.DisableHtmlEmail + ? ui.Text("notifications", "mailBody", strings, mailingUser) + : ui.Text("notifications", "mailBodyHtml", strings, mailingUser)); + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Strategies/NotificationsHandler.cs b/src/Umbraco.Web/Strategies/NotificationsHandler.cs index 3eb750c1fd..a085bd463a 100644 --- a/src/Umbraco.Web/Strategies/NotificationsHandler.cs +++ b/src/Umbraco.Web/Strategies/NotificationsHandler.cs @@ -9,6 +9,7 @@ using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Services; using umbraco; using umbraco.BusinessLogic.Actions; +using Umbraco.Core.Models; namespace Umbraco.Web.Strategies { @@ -36,6 +37,9 @@ namespace Umbraco.Web.Strategies //Send notifications for the update and created actions ContentService.Saved += (sender, args) => { + var newEntities = new List(); + var updatedEntities = new List(); + //need to determine if this is updating or if it is new foreach (var entity in args.SavedEntities) { @@ -43,16 +47,16 @@ namespace Umbraco.Web.Strategies if (dirty.WasPropertyDirty("Id")) { //it's new - applicationContext.Services.NotificationService.SendNotification( - entity, ActionNew.Instance, applicationContext); + newEntities.Add(entity); } else { //it's updating - applicationContext.Services.NotificationService.SendNotification( - entity, ActionUpdate.Instance, applicationContext); + updatedEntities.Add(entity); } } + applicationContext.Services.NotificationService.SendNotification(newEntities, ActionNew.Instance, applicationContext); + applicationContext.Services.NotificationService.SendNotification(updatedEntities, ActionUpdate.Instance, applicationContext); }; //Send notifications for the delete action diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/nodeSorter.asmx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/nodeSorter.asmx.cs index 1ab2346054..734894a00a 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/nodeSorter.asmx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/nodeSorter.asmx.cs @@ -177,13 +177,15 @@ namespace umbraco.presentation.webservices var sortedContent = new List(); try { - for (var i = 0; i < ids.Length; i++) - { - var id = int.Parse(ids[i]); - var c = contentService.GetById(id); - sortedContent.Add(c); - } + int [] intIds = ids.Select(id => int.Parse(id)).ToArray(); + sortedContent = contentService.GetByIds(intIds).ToList(); + + sortedContent = (from id in intIds + join content in sortedContent + on id equals content.Id + select content).ToList(); + // Save content with new sort order and update db+cache accordingly var sorted = contentService.Sort(sortedContent);