Merge pull request #1451 from umbraco/temp-u4-8698

U4-8698 - fix notification service perfs issues
This commit is contained in:
Stephan
2016-10-06 08:31:39 +02:00
committed by GitHub
10 changed files with 400 additions and 152 deletions

View File

@@ -69,6 +69,11 @@ namespace Umbraco.Core
/// </summary>
public const string Document = "C66BA18E-EAF3-4CFF-8A22-41B16D66A972";
/// <summary>
/// Guid for a Document object.
/// </summary>
public static readonly Guid DocumentGuid = new Guid(Document);
/// <summary>
/// Guid for a Document Type object.
/// </summary>

View File

@@ -34,11 +34,19 @@ namespace Umbraco.Core.Persistence.Repositories
int CountDescendants(int parentId, string contentTypeAlias = null);
/// <summary>
/// Gets a list of all versions for an <see cref="TEntity"/>.
/// Gets a list of all versions for an <see cref="TEntity"/> ordered so latest is first
/// </summary>
/// <param name="id">Id of the <see cref="TEntity"/> to retrieve versions from</param>
/// <returns>An enumerable list of the same <see cref="TEntity"/> object with different versions</returns>
IEnumerable<TEntity> GetAllVersions(int id);
IEnumerable<TEntity> GetAllVersions(int id);
/// <summary>
/// Gets a list of all version Ids for the given content item
/// </summary>
/// <param name="id"></param>
/// <param name="maxRows">The maximum number of rows to return</param>
/// <returns></returns>
IEnumerable<Guid> GetVersionIds(int id, int maxRows);
/// <summary>
/// Gets a specific version of an <see cref="TEntity"/>.

View File

@@ -18,6 +18,27 @@ namespace Umbraco.Core.Persistence.Repositories
_unitOfWork = unitOfWork;
}
public IEnumerable<Notification> GetUsersNotifications(IEnumerable<int> userIds, string action, IEnumerable<int> nodeIds, Guid objectType)
{
var nodeIdsA = nodeIds.ToArray();
var syntax = ApplicationContext.Current.DatabaseContext.SqlSyntax; // bah
var sql = new Sql()
.Select("DISTINCT umbracoNode.id nodeId, umbracoUser.id userId, umbracoNode.nodeObjectType, umbracoUser2NodeNotify.action")
.From<User2NodeNotifyDto>(syntax)
.InnerJoin<NodeDto>(syntax).On<User2NodeNotifyDto, NodeDto>(syntax, left => left.NodeId, right => right.NodeId)
.InnerJoin<UserDto>(syntax).On<User2NodeNotifyDto, UserDto>(syntax, left => left.UserId, right => right.Id)
.Where<NodeDto>(x => x.NodeObjectType == objectType)
.Where<UserDto>(x => x.Disabled == false) // only approved users
.Where<User2NodeNotifyDto>(x => x.Action == action); // on the specified action
if (nodeIdsA.Length > 0)
sql
.WhereIn<NodeDto>(x => x.NodeId, nodeIdsA); // for the specified nodes
sql
.OrderBy<UserDto>(x => x.Id, syntax)
.OrderBy<NodeDto>(dto => dto.NodeId, syntax);
return _unitOfWork.Database.Fetch<dynamic>(sql).Select(x => new Notification(x.nodeId, x.userId, x.action, objectType));
}
public IEnumerable<Notification> GetUserNotifications(IUser user)
{
var sql = new Sql()

View File

@@ -320,51 +320,50 @@ namespace Umbraco.Core.Persistence.Repositories
/// </remarks>
public IEnumerable<IUser> GetPagedResultsByQuery(IQuery<IUser> query, int pageIndex, int pageSize, out int totalRecords, Expression<Func<IUser, string>> orderBy)
{
if (orderBy == null) throw new ArgumentNullException("orderBy");
var sql = new Sql();
sql.Select("*").From<UserDto>();
Sql resultQuery;
if (query != null)
{
var translator = new SqlTranslator<IUser>(sql, query);
resultQuery = translator.Translate();
}
else
{
resultQuery = sql;
}
//get the referenced column name
// get the referenced column name and find the corresp mapped column name
var expressionMember = ExpressionHelper.GetMemberInfo(orderBy);
//now find the mapped column name
var mapper = MappingResolver.Current.ResolveMapperByType(typeof(IUser));
var mappedField = mapper.Map(expressionMember.Name);
if (orderBy == null)
throw new ArgumentNullException("orderBy");
if (mappedField.IsNullOrWhiteSpace())
{
throw new ArgumentException("Could not find a mapping for the column specified in the orderBy clause");
}
//need to ensure the order by is in brackets, see: https://github.com/toptensoftware/PetaPoco/issues/177
resultQuery.OrderBy(string.Format("({0})", mappedField));
var pagedResult = Database.Page<UserDto>(pageIndex + 1, pageSize, resultQuery);
var sql = new Sql()
.Select("umbracoUser.Id")
.From<UserDto>(SqlSyntax);
totalRecords = Convert.ToInt32(pagedResult.TotalItems);
var idsQuery = query == null ? sql : new SqlTranslator<IUser>(sql, query).Translate();
// need to ensure the order by is in brackets, see: https://github.com/toptensoftware/PetaPoco/issues/177
idsQuery.OrderBy("(" + mappedField + ")");
var page = Database.Page<int>(pageIndex + 1, pageSize, idsQuery);
totalRecords = Convert.ToInt32(page.TotalItems);
//now that we have the user dto's we need to construct true members from the list.
if (totalRecords == 0)
{
return Enumerable.Empty<IUser>();
}
var ids = pagedResult.Items.Select(x => x.Id).ToArray();
var result = ids.Length == 0 ? Enumerable.Empty<IUser>() : GetAll(ids);
//now we need to ensure this result is also ordered by the same order by clause
return result.OrderBy(orderBy.Compile());
// now get the actual users and ensure they are ordered properly (same clause)
var ids = page.Items.ToArray();
return ids.Length == 0 ? Enumerable.Empty<IUser>() : GetAll(ids).OrderBy(orderBy.Compile());
}
internal IEnumerable<IUser> GetNextUsers(int id, int count)
{
var idsQuery = new Sql()
.Select("umbracoUser.Id")
.From<UserDto>(SqlSyntax)
.Where<UserDto>(x => x.Id >= id)
.OrderBy<UserDto>(x => x.Id, SqlSyntax);
// first page is index 1, not zero
var ids = Database.Page<int>(1, count, idsQuery).Items.ToArray();
// now get the actual users and ensure they are ordered properly (same clause)
return ids.Length == 0 ? Enumerable.Empty<IUser>() : GetAll(ids).OrderBy(x => x.Id);
}
/// <summary>
/// Returns permissions for a given user for any number of nodes
/// </summary>

View File

@@ -40,6 +40,11 @@ namespace Umbraco.Core.Persistence.Repositories
#region IRepositoryVersionable Implementation
/// <summary>
/// Gets a list of all versions for an <see cref="TEntity"/> ordered so latest is first
/// </summary>
/// <param name="id">Id of the <see cref="TEntity"/> to retrieve versions from</param>
/// <returns>An enumerable list of the same <see cref="TEntity"/> object with different versions</returns>
public virtual IEnumerable<TEntity> GetAllVersions(int id)
{
var sql = new Sql();
@@ -60,6 +65,28 @@ namespace Umbraco.Core.Persistence.Repositories
}
}
/// <summary>
/// Gets a list of all version Ids for the given content item ordered so latest is first
/// </summary>
/// <param name="id"></param>
/// <param name="maxRows">The maximum number of rows to return</param>
/// <returns></returns>
public virtual IEnumerable<Guid> GetVersionIds(int id, int maxRows)
{
var sql = new Sql();
sql.Select("cmsDocument.versionId")
.From<DocumentDto>(SqlSyntax)
.InnerJoin<ContentDto>(SqlSyntax)
.On<DocumentDto, ContentDto>(SqlSyntax, left => left.NodeId, right => right.NodeId)
.InnerJoin<NodeDto>(SqlSyntax)
.On<ContentDto, NodeDto>(SqlSyntax, left => left.NodeId, right => right.NodeId)
.Where<NodeDto>(x => x.NodeObjectType == NodeObjectTypeId)
.Where<NodeDto>(x => x.NodeId == id)
.OrderByDescending<DocumentDto>(x => x.UpdateDate, SqlSyntax);
return Database.Fetch<Guid>(SqlSyntax.SelectTop(sql, maxRows));
}
public virtual void DeleteVersion(Guid versionId)
{
var dto = Database.FirstOrDefault<ContentVersionDto>("WHERE versionId = @VersionId", new { VersionId = versionId });

View File

@@ -448,6 +448,21 @@ namespace Umbraco.Core.Services
}
}
/// <summary>
/// Gets a list of all version Ids for the given content item ordered so latest is first
/// </summary>
/// <param name="id"></param>
/// <param name="maxRows">The maximum number of rows to return</param>
/// <returns></returns>
public IEnumerable<Guid> GetVersionIds(int id, int maxRows)
{
using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork()))
{
var versions = repository.GetVersionIds(id, maxRows);
return versions;
}
}
/// <summary>
/// Gets a collection of <see cref="IContent"/> objects, which are ancestors of the current content.
/// </summary>

View File

@@ -298,6 +298,14 @@ namespace Umbraco.Core.Services
/// <returns>An Enumerable list of <see cref="IContent"/> objects</returns>
IEnumerable<IContent> GetVersions(int id);
/// <summary>
/// Gets a list of all version Ids for the given content item ordered so latest is first
/// </summary>
/// <param name="id"></param>
/// <param name="maxRows">The maximum number of rows to return</param>
/// <returns></returns>
IEnumerable<Guid> GetVersionIds(int id, int maxRows);
/// <summary>
/// Gets a collection of <see cref="IContent"/> objects, which reside at the first level / root
/// </summary>

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
@@ -15,7 +16,6 @@ using Umbraco.Core.Models.Membership;
using Umbraco.Core.Persistence.Repositories;
using Umbraco.Core.Persistence.UnitOfWork;
using Umbraco.Core.Strings;
using umbraco.interfaces;
namespace Umbraco.Core.Services
{
@@ -55,38 +55,72 @@ namespace Umbraco.Core.Services
Func<IUser, string[], string> createSubject,
Func<IUser, string[], string> createBody)
{
if ((entity is IContent) == false)
if (entity is IContent == false)
throw new NotSupportedException();
var content = (IContent) entity;
// lazily get versions - into a list to ensure we can enumerate multiple times
List<IContent> allVersions = null;
var content = (IContent) entity;
int totalUsers;
var allUsers = _userService.GetAll(0, int.MaxValue, out totalUsers);
foreach (var u in allUsers.Where(x => x.IsApproved))
// lazily get previous version
IContentBase prevVersion = null;
// do not load *all* users in memory at once
// do not load notifications *per user* (N+1 select)
// cannot load users & notifications in 1 query (combination btw User2AppDto and User2NodeNotifyDto)
// => get batches of users, get all their notifications in 1 query
// re. users:
// users being (dis)approved = not an issue, filtered in memory not in SQL
// users being modified or created = not an issue, ordering by ID, as long as we don't *insert* low IDs
// users being deleted = not an issue for GetNextUsers
var id = 0;
var nodeIds = content.Path.Split(',').Select(int.Parse).ToArray();
const int pagesz = 400; // load batches of 400 users
do
{
var userNotifications = GetUserNotifications(u, content.Path);
var notificationForAction = userNotifications.FirstOrDefault(x => x.Action == action);
if (notificationForAction == null) continue;
// users are returned ordered by id, notifications are returned ordered by user id
var users = ((UserService) _userService).GetNextUsers(id, pagesz).Where(x => x.IsApproved).ToList();
var notifications = GetUsersNotifications(users.Select(x => x.Id), action, nodeIds, Constants.ObjectTypes.DocumentGuid).ToList();
if (notifications.Count == 0) break;
if (allVersions == null) // lazy load
allVersions = _contentService.GetVersions(entity.Id).ToList();
try
var i = 0;
foreach (var user in users)
{
SendNotification(operatingUser, u, content, allVersions,
actionName, http, createSubject, createBody);
// continue if there's no notification for this user
if (notifications[i].UserId != user.Id) continue; // next user
_logger.Debug<NotificationService>(string.Format("Notification type: {0} sent to {1} ({2})",
action, u.Name, u.Email));
// lazy load prev version
if (prevVersion == null)
{
prevVersion = GetPreviousVersion(entity.Id);
}
// queue notification
var req = CreateNotificationRequest(operatingUser, user, content, prevVersion, actionName, http, createSubject, createBody);
Enqueue(req);
// skip other notifications for this user
while (i < notifications.Count && notifications[i++].UserId == user.Id) ;
if (i >= notifications.Count) break; // break if no more notifications
}
catch (Exception ex)
{
_logger.Error<NotificationService>("An error occurred sending notification", ex);
}
}
// load more users if any
id = users.Count == pagesz ? users.Last().Id + 1 : -1;
} while (id > 0);
}
/// <summary>
/// Gets the previous version to the latest version of the content item if there is one
/// </summary>
/// <param name="contentId"></param>
/// <returns></returns>
private IContentBase GetPreviousVersion(int contentId)
{
// Regarding this: http://issues.umbraco.org/issue/U4-5180
// we know they are descending from the service so we know that newest is first
// we are only selecting the top 2 rows since that is all we need
var allVersions = _contentService.GetVersionIds(contentId, 2).ToList();
var prevVersionIndex = allVersions.Count > 1 ? 1 : 0;
return _contentService.GetByVersion(allVersions[prevVersionIndex]);
}
/// <summary>
@@ -106,47 +140,76 @@ namespace Umbraco.Core.Services
Func<IUser, string[], string> createSubject,
Func<IUser, string[], string> createBody)
{
if ((entities is IEnumerable<IContent>) == false)
if (entities is IEnumerable<IContent> == false)
throw new NotSupportedException();
// ensure we can enumerate multiple times
var entitiesL = entities as List<IContent> ?? entities.Cast<IContent>().ToList();
// lazily get versions - into lists to ensure we can enumerate multiple times
var allVersionsDictionary = new Dictionary<int, List<IContent>>();
//exit if there are no entities
if (entitiesL.Count == 0) return;
int totalUsers;
var allUsers = _userService.GetAll(0, int.MaxValue, out totalUsers);
foreach (var u in allUsers.Where(x => x.IsApproved))
//put all entity's paths into a list with the same indicies
var paths = entitiesL.Select(x => x.Path.Split(',').Select(int.Parse).ToArray()).ToArray();
// lazily get versions
var prevVersionDictionary = new Dictionary<int, IContentBase>();
// see notes above
var id = 0;
const int pagesz = 400; // load batches of 400 users
do
{
var userNotifications = GetUserNotifications(u).ToArray();
foreach (var content in entitiesL)
// users are returned ordered by id, notifications are returned ordered by user id
var users = ((UserService)_userService).GetNextUsers(id, pagesz).Where(x => x.IsApproved).ToList();
var notifications = GetUsersNotifications(users.Select(x => x.Id), action, Enumerable.Empty<int>(), Constants.ObjectTypes.DocumentGuid).ToList();
if (notifications.Count == 0) break;
var i = 0;
foreach (var user in users)
{
var userNotificationsByPath = FilterUserNotificationsByPath(userNotifications, content.Path);
var notificationForAction = userNotificationsByPath.FirstOrDefault(x => x.Action == action);
if (notificationForAction == null) continue;
// continue if there's no notification for this user
if (notifications[i].UserId != user.Id) continue; // next user
var allVersions = allVersionsDictionary.ContainsKey(content.Id) // lazy load
? allVersionsDictionary[content.Id]
: allVersionsDictionary[content.Id] = _contentService.GetVersions(content.Id).ToList();
try
for (var j = 0; j < entitiesL.Count; j++)
{
SendNotification(operatingUser, u, content, allVersions,
actionName, http, createSubject, createBody);
var content = entitiesL[j];
var path = paths[j];
// test if the notification applies to the path ie to this entity
if (path.Contains(notifications[i].EntityId) == false) continue; // next entity
if (prevVersionDictionary.ContainsKey(content.Id) == false)
{
prevVersionDictionary[content.Id] = GetPreviousVersion(content.Id);
}
// queue notification
var req = CreateNotificationRequest(operatingUser, user, content, prevVersionDictionary[content.Id], actionName, http, createSubject, createBody);
Enqueue(req);
}
_logger.Debug<NotificationService>(string.Format("Notification type: {0} sent to {1} ({2})",
action, u.Name, u.Email));
}
catch (Exception ex)
// skip other notifications for this user, essentially this means moving i to the next index of notifications
// for the next user.
do
{
_logger.Error<NotificationService>("An error occurred sending notification", ex);
}
}
}
i++;
} while (i < notifications.Count && notifications[i].UserId == user.Id);
if (i >= notifications.Count) break; // break if no more notifications
}
// load more users if any
id = users.Count == pagesz ? users.Last().Id + 1 : -1;
} while (id > 0);
}
private IEnumerable<Notification> GetUsersNotifications(IEnumerable<int> userIds, string action, IEnumerable<int> nodeIds, Guid objectType)
{
var uow = _uowProvider.GetUnitOfWork();
var repository = new NotificationsRepository(uow);
return repository.GetUsersNotifications(userIds, action, nodeIds, objectType);
}
/// <summary>
/// Gets the notifications for the user
@@ -184,7 +247,7 @@ namespace Umbraco.Core.Services
public IEnumerable<Notification> FilterUserNotificationsByPath(IEnumerable<Notification> userNotifications, string path)
{
var pathParts = path.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries);
return userNotifications.Where(r => pathParts.InvariantContains(r.EntityId.ToString(CultureInfo.InvariantCulture))).ToList();
return userNotifications.Where(r => pathParts.InvariantContains(r.EntityId.ToString(CultureInfo.InvariantCulture))).ToList();
}
/// <summary>
@@ -254,29 +317,23 @@ namespace Umbraco.Core.Services
/// <param name="performingUser"></param>
/// <param name="mailingUser"></param>
/// <param name="content"></param>
/// <param name="allVersions"></param>
/// <param name="oldDoc"></param>
/// <param name="actionName">The action readable name - currently an action is just a single letter, this is the name associated with the letter </param>
/// <param name="http"></param>
/// <param name="createSubject">Callback to create the mail subject</param>
/// <param name="createBody">Callback to create the mail body</param>
private void SendNotification(IUser performingUser, IUser mailingUser, IContent content, IEnumerable<IContent> allVersions, string actionName, HttpContextBase http,
private NotificationRequest CreateNotificationRequest(IUser performingUser, IUser mailingUser, IContentBase content, IContentBase oldDoc,
string actionName, HttpContextBase http,
Func<IUser, string[], string> createSubject,
Func<IUser, string[], string> createBody)
{
if (performingUser == null) throw new ArgumentNullException("performingUser");
if (mailingUser == null) throw new ArgumentNullException("mailingUser");
if (content == null) throw new ArgumentNullException("content");
if (allVersions == null) throw new ArgumentNullException("allVersions");
if (http == null) throw new ArgumentNullException("http");
if (createSubject == null) throw new ArgumentNullException("createSubject");
if (createBody == null) throw new ArgumentNullException("createBody");
//Ensure they are sorted: http://issues.umbraco.org/issue/U4-5180
var allVersionsAsArray = allVersions.OrderBy(x => x.UpdateDate).ToArray();
int versionCount = (allVersionsAsArray.Length > 1) ? (allVersionsAsArray.Length - 2) : (allVersionsAsArray.Length - 1);
var oldDoc = _contentService.GetByVersion(allVersionsAsArray[versionCount].Version);
if (createBody == null) throw new ArgumentNullException("createBody");
// build summary
var summary = new StringBuilder();
var props = content.Properties.ToArray();
@@ -290,16 +347,16 @@ namespace Umbraco.Core.Services
{
var oldProperty = oldDoc.Properties[p.PropertyType.Alias];
oldText = oldProperty.Value != null ? oldProperty.Value.ToString() : "";
// replace html with char equivalent
ReplaceHtmlSymbols(ref oldText);
ReplaceHtmlSymbols(ref newText);
}
// make sure to only highlight changes done using TinyMCE editor... other changes will be displayed using default summary
// TODO: We should probably allow more than just tinymce??
if ((p.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.TinyMCEAlias)
if ((p.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.TinyMCEAlias)
&& string.CompareOrdinal(oldText, newText) != 0)
{
summary.Append("<tr>");
@@ -308,26 +365,31 @@ namespace Umbraco.Core.Services
"<td style='text-align: left; vertical-align: top;'> <span style='background-color:red;'>Red for deleted characters</span>&nbsp;<span style='background-color:yellow;'>Yellow for inserted characters</span></td>");
summary.Append("</tr>");
summary.Append("<tr>");
summary.Append("<th style='text-align: left; vertical-align: top; width: 25%;'> New " +
p.PropertyType.Name + "</th>");
summary.Append("<td style='text-align: left; vertical-align: top;'>" +
ReplaceLinks(CompareText(oldText, newText, true, false, "<span style='background-color:yellow;'>", string.Empty), http.Request) +
"</td>");
summary.Append("<th style='text-align: left; vertical-align: top; width: 25%;'> New ");
summary.Append(p.PropertyType.Name);
summary.Append("</th>");
summary.Append("<td style='text-align: left; vertical-align: top;'>");
summary.Append(ReplaceLinks(CompareText(oldText, newText, true, false, "<span style='background-color:yellow;'>", string.Empty), http.Request));
summary.Append("</td>");
summary.Append("</tr>");
summary.Append("<tr>");
summary.Append("<th style='text-align: left; vertical-align: top; width: 25%;'> Old " +
p.PropertyType.Name + "</th>");
summary.Append("<td style='text-align: left; vertical-align: top;'>" +
ReplaceLinks(CompareText(newText, oldText, true, false, "<span style='background-color:red;'>", string.Empty), http.Request) +
"</td>");
summary.Append("<th style='text-align: left; vertical-align: top; width: 25%;'> Old ");
summary.Append(p.PropertyType.Name);
summary.Append("</th>");
summary.Append("<td style='text-align: left; vertical-align: top;'>");
summary.Append(ReplaceLinks(CompareText(newText, oldText, true, false, "<span style='background-color:red;'>", string.Empty), http.Request));
summary.Append("</td>");
summary.Append("</tr>");
}
else
{
summary.Append("<tr>");
summary.Append("<th style='text-align: left; vertical-align: top; width: 25%;'>" +
p.PropertyType.Name + "</th>");
summary.Append("<td style='text-align: left; vertical-align: top;'>" + newText + "</td>");
summary.Append("<th style='text-align: left; vertical-align: top; width: 25%;'>");
summary.Append(p.PropertyType.Name);
summary.Append("</th>");
summary.Append("<td style='text-align: left; vertical-align: top;'>");
summary.Append(newText);
summary.Append("</td>");
summary.Append("</tr>");
}
summary.Append(
@@ -338,29 +400,27 @@ namespace Umbraco.Core.Services
string[] subjectVars = {
http.Request.ServerVariables["SERVER_NAME"] + ":" +
http.Request.Url.Port +
IOHelper.ResolveUrl(SystemDirectories.Umbraco),
string.Concat(http.Request.ServerVariables["SERVER_NAME"], ":", http.Request.Url.Port, IOHelper.ResolveUrl(SystemDirectories.Umbraco)),
actionName,
content.Name
};
string[] bodyVars = {
mailingUser.Name,
actionName,
content.Name,
mailingUser.Name,
actionName,
content.Name,
performingUser.Name,
http.Request.ServerVariables["SERVER_NAME"] + ":" + http.Request.Url.Port + IOHelper.ResolveUrl(SystemDirectories.Umbraco),
string.Concat(http.Request.ServerVariables["SERVER_NAME"], ":", http.Request.Url.Port, IOHelper.ResolveUrl(SystemDirectories.Umbraco)),
content.Id.ToString(CultureInfo.InvariantCulture), summary.ToString(),
string.Format("{2}://{0}/{1}",
http.Request.ServerVariables["SERVER_NAME"] + ":" + http.Request.Url.Port,
string.Concat(http.Request.ServerVariables["SERVER_NAME"], ":", http.Request.Url.Port),
//TODO: RE-enable this so we can have a nice url
/*umbraco.library.NiceUrl(documentObject.Id))*/
content.Id + ".aspx",
string.Concat(content.Id, ".aspx"),
protocol)
};
// create the mail message
// create the mail message
var mail = new MailMessage(UmbracoConfig.For.UmbracoSettings().Content.NotificationEmailAddress, mailingUser.Email);
// populate the message
@@ -374,10 +434,10 @@ namespace Umbraco.Core.Services
{
mail.IsBodyHtml = true;
mail.Body =
@"<html><head>
string.Concat(@"<html><head>
</head>
<body style='font-family: Trebuchet MS, arial, sans-serif; font-color: black;'>
" + createBody(mailingUser, bodyVars);
", createBody(mailingUser, bodyVars));
}
// nh, issue 30724. Due to hardcoded http strings in resource files, we need to check for https replacements here
@@ -390,32 +450,17 @@ namespace Umbraco.Core.Services
string.Format("https://{0}", serverName));
}
// send it asynchronously, we don't want to got up all of the request time to send emails!
ThreadPool.QueueUserWorkItem(state =>
{
try
{
using (mail)
{
using (var sender = new SmtpClient())
{
sender.Send(mail);
}
}
}
catch (Exception ex)
{
_logger.Error<NotificationService>("An error occurred sending notification", ex);
}
});
return new NotificationRequest(mail, actionName, mailingUser.Name, mailingUser.Email);
}
private static string ReplaceLinks(string text, HttpRequestBase request)
{
string domain = GlobalSettings.UseSSL ? "https://" : "http://";
domain += request.ServerVariables["SERVER_NAME"] + ":" + request.Url.Port + "/";
var sb = new StringBuilder(GlobalSettings.UseSSL ? "https://" : "http://");
sb.Append(request.ServerVariables["SERVER_NAME"]);
sb.Append(":");
sb.Append(request.Url.Port);
sb.Append("/");
var domain = sb.ToString();
text = text.Replace("href=\"/", "href=\"" + domain);
text = text.Replace("src=\"/", "src=\"" + domain);
return text;
@@ -484,7 +529,7 @@ namespace Umbraco.Core.Services
pos++;
} // while
sb.Append("</span>");
} // if
} // if
} // while
// write rest of unchanged chars
@@ -495,8 +540,95 @@ namespace Umbraco.Core.Services
} // while
return sb.ToString();
}
// manage notifications
// ideally, would need to use IBackgroundTasks - but they are not part of Core!
private static readonly object Locker = new object();
private static readonly BlockingCollection<NotificationRequest> Queue = new BlockingCollection<NotificationRequest>();
private static volatile bool _running;
private void Enqueue(NotificationRequest notification)
{
Queue.Add(notification);
if (_running) return;
lock (Locker)
{
if (_running) return;
Process(Queue);
_running = true;
}
}
private class NotificationRequest
{
public NotificationRequest(MailMessage mail, string action, string userName, string email)
{
Mail = mail;
Action = action;
UserName = userName;
Email = email;
}
public MailMessage Mail { get; private set; }
public string Action { get; private set; }
public string UserName { get; private set; }
public string Email { get; private set; }
}
private void Process(BlockingCollection<NotificationRequest> notificationRequests)
{
ThreadPool.QueueUserWorkItem(state =>
{
var s = new SmtpClient();
try
{
_logger.Debug<NotificationService>("Begin processing notifications.");
while (true)
{
NotificationRequest request;
while (notificationRequests.TryTake(out request, 8 * 1000)) // stay on for 8s
{
try
{
if (Sendmail != null) Sendmail(s, request.Mail, _logger); else s.Send(request.Mail);
_logger.Debug<NotificationService>(string.Format("Notification \"{0}\" sent to {1} ({2})", request.Action, request.UserName, request.Email));
}
catch (Exception ex)
{
_logger.Error<NotificationService>("An error occurred sending notification", ex);
s.Dispose();
s = new SmtpClient();
}
finally
{
request.Mail.Dispose();
}
}
lock (Locker)
{
if (notificationRequests.Count > 0) continue; // last chance
_running = false; // going down
break;
}
}
}
finally
{
s.Dispose();
}
_logger.Debug<NotificationService>("Done processing notifications.");
});
}
// for tests
internal static Action<SmtpClient, MailMessage, ILogger> Sendmail;
//= (_, msg, logger) => logger.Debug<NotificationService>("Email " + msg.To.ToString());
#endregion
}
}

View File

@@ -9,6 +9,7 @@ using Umbraco.Core.Logging;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Persistence;
using Umbraco.Core.Persistence.Querying;
using Umbraco.Core.Persistence.Repositories;
using Umbraco.Core.Persistence.UnitOfWork;
using Umbraco.Core.Security;
@@ -506,6 +507,15 @@ namespace Umbraco.Core.Services
}
}
internal IEnumerable<IUser> GetNextUsers(int id, int count)
{
var uow = UowProvider.GetUnitOfWork();
using (var repository = (UserRepository) RepositoryFactory.CreateUserRepository(uow))
{
return repository.GetNextUsers(id, count);
}
}
#endregion
#region Implementation of IUserService

View File

@@ -68,6 +68,29 @@ namespace Umbraco.Tests.Services
Assert.IsTrue(contentService.PublishWithStatus(content).Success);
}
[Test]
public void Get_Top_Version_Ids()
{
// Arrange
var contentService = ServiceContext.ContentService;
// Act
var content = contentService.CreateContentWithIdentity("Test", -1, "umbTextpage", 0);
for (int i = 0; i < 20; i++)
{
content.SetValue("bodyText", "hello world " + Guid.NewGuid());
contentService.SaveAndPublishWithStatus(content);
}
// Assert
var allVersions = contentService.GetVersionIds(content.Id, int.MaxValue);
Assert.AreEqual(21, allVersions.Count());
var topVersions = contentService.GetVersionIds(content.Id, 4);
Assert.AreEqual(4, topVersions.Count());
}
[Test]
public void Get_By_Ids_Sorted()
{