Creates data integrity health checks

This commit is contained in:
Shannon
2020-04-07 01:02:08 +10:00
parent 50e9f79ae6
commit 4b467bf470
8 changed files with 326 additions and 3 deletions

View File

@@ -77,5 +77,12 @@ namespace Umbraco.Core.Persistence.Repositories
/// <remarks>Here, <paramref name="filter"/> can be null but <paramref name="ordering"/> cannot.</remarks>
IEnumerable<TEntity> GetPage(IQuery<TEntity> query, long pageIndex, int pageSize, out long totalRecords,
IQuery<TEntity> filter, Ordering ordering);
/// <summary>
/// Checks the data integrity of the node paths stored in the database
/// </summary>
bool VerifyNodePaths(out int[] invalidIds);
void FixNodePaths();
}
}

View File

@@ -403,7 +403,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
}
// content type alias is invariant
if(ordering.OrderBy.InvariantEquals("contentTypeAlias"))
if (ordering.OrderBy.InvariantEquals("contentTypeAlias"))
{
var joins = Sql()
.InnerJoin<ContentTypeDto>("ctype").On<ContentDto, ContentTypeDto>((content, contentType) => content.ContentTypeId == contentType.NodeId, aliasRight: "ctype");
@@ -477,6 +477,169 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
IQuery<TEntity> filter,
Ordering ordering);
public bool VerifyNodePaths(out int[] invalidIds)
{
var invalid = new List<int>();
var sql = SqlContext.Sql()
.Select<NodeDto>()
.From<NodeDto>()
.Where<NodeDto>(x => x.NodeObjectType == NodeObjectTypeId)
.OrderBy<NodeDto>(x => x.Level, x => x.ParentId, x => x.SortOrder);
// TODO: Could verify sort orders here too
var currentParentIds = new HashSet<int> { -1 };
var prevParentIds = currentParentIds;
var lastLevel = -1;
// use a forward cursor (query)
foreach (var node in Database.Query<NodeDto>(sql))
{
if (node.Level != lastLevel)
{
// changing levels
prevParentIds = currentParentIds;
currentParentIds = null;
lastLevel = node.Level;
}
if (currentParentIds == null)
{
// we're reset
currentParentIds = new HashSet<int>();
}
currentParentIds.Add(node.NodeId);
var pathParts = node.Path.Split(',');
if (!prevParentIds.Contains(node.ParentId))
{
// invalid, this will be because the level is wrong
invalid.Add(node.NodeId);
}
else if (pathParts.Length < 2)
{
// invalid path
invalid.Add(node.NodeId);
}
else if (pathParts.Length - 1 != node.Level)
{
// invalid, either path or level is wrong
invalid.Add(node.NodeId);
}
else if (pathParts[pathParts.Length - 1] != node.NodeId.ToString())
{
// invalid path
invalid.Add(node.NodeId);
}
else if (pathParts[pathParts.Length - 2] != node.ParentId.ToString())
{
// invalid path
invalid.Add(node.NodeId);
}
}
invalidIds = invalid.ToArray();
return invalid.Count == 0;
}
public void FixNodePaths()
{
// TODO: We can probably combine this logic with the above
var invalid = new List<(int child, int parent)>();
var sql = SqlContext.Sql()
.Select<NodeDto>()
.From<NodeDto>()
.Where<NodeDto>(x => x.NodeObjectType == NodeObjectTypeId)
.OrderBy<NodeDto>(x => x.Level, x => x.ParentId, x => x.SortOrder);
// TODO: Could verify sort orders here too
var updated = new List<NodeDto>();
var missingParentIds = new Dictionary<int, List<NodeDto>>();
var currentParentIds = new HashSet<int> { -1 };
var prevParentIds = currentParentIds;
var lastLevel = -1;
// use a forward cursor (query)
foreach (var node in Database.Query<NodeDto>(sql))
{
if (node.Level != lastLevel)
{
// changing levels
prevParentIds = currentParentIds;
currentParentIds = null;
lastLevel = node.Level;
}
if (currentParentIds == null)
{
// we're reset
currentParentIds = new HashSet<int>();
}
currentParentIds.Add(node.NodeId);
var pathParts = node.Path.Split(',');
if (!prevParentIds.Contains(node.ParentId))
{
// invalid, this will be because the level is wrong (which prob means path is wrong too)
invalid.Add((node.NodeId, node.ParentId));
if (missingParentIds.TryGetValue(node.ParentId, out var childIds))
childIds.Add(node);
else
missingParentIds[node.ParentId] = new List<NodeDto> {node};
}
else if (pathParts.Length < 2)
{
// invalid path
invalid.Add((node.NodeId, node.ParentId));
}
else if (pathParts.Length - 1 != node.Level)
{
// invalid, either path or level is wrong
invalid.Add((node.NodeId, node.ParentId));
}
else if (pathParts[pathParts.Length - 1] != node.NodeId.ToString())
{
// invalid path
invalid.Add((node.NodeId, node.ParentId));
}
else if (pathParts[pathParts.Length - 2] != node.ParentId.ToString())
{
// invalid path
invalid.Add((node.NodeId, node.ParentId));
}
else
{
// it's valid
if (missingParentIds.TryGetValue(node.NodeId, out var invalidNodes))
{
// this parent has been flagged as missing which means one or more of it's children was ordered
// wrong and was checked first. So now we can try to rebuild the invalid paths.
foreach (var invalidNode in invalidNodes)
{
invalidNode.Level = (short) (node.Level + 1);
invalidNode.Path = node.Path + "," + invalidNode.NodeId;
updated.Add(invalidNode);
}
}
}
}
foreach (var node in updated)
{
Database.Update(node);
}
}
// here, filter can be null and ordering cannot
protected IEnumerable<TEntity> GetPage<TDto>(IQuery<TEntity> query,
long pageIndex, int pageSize, out long totalRecords,

View File

@@ -526,6 +526,6 @@ namespace Umbraco.Core.Services
OperationResult Rollback(int id, int versionId, string culture = "*", int userId = Constants.Security.SuperUserId);
#endregion
}
}

View File

@@ -5,5 +5,16 @@
/// TODO: Start sharing the logic!
/// </summary>
public interface IContentServiceBase : IService
{ }
{
/// <summary>
/// Checks the data integrity of the node paths/levels stored in the database
/// </summary>
bool VerifyNodePaths(out int[] invalidIds);
/// <summary>
/// Fixes the data integrity of node paths/levels stored in the database
/// </summary>
void FixNodePaths();
}
}

View File

@@ -2375,6 +2375,26 @@ namespace Umbraco.Core.Services.Implement
return OperationResult.Succeed(evtMsgs);
}
public bool VerifyNodePaths(out int[] invalidIds)
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
{
scope.ReadLock(Constants.Locks.ContentTree);
return _documentRepository.VerifyNodePaths(out invalidIds);
}
}
public void FixNodePaths()
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
{
scope.WriteLock(Constants.Locks.ContentTree);
_documentRepository.FixNodePaths();
// TODO: We're going to have to clear all caches
}
}
#endregion
#region Internal Methods

View File

@@ -1139,6 +1139,28 @@ namespace Umbraco.Core.Services.Implement
}
return true;
}
public bool VerifyNodePaths(out int[] invalidIds)
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
{
scope.ReadLock(Constants.Locks.MediaTree);
return _mediaRepository.VerifyNodePaths(out invalidIds);
}
}
public void FixNodePaths()
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
{
scope.WriteLock(Constants.Locks.MediaTree);
_mediaRepository.FixNodePaths();
// TODO: We're going to have to clear all caches
}
}
#endregion
@@ -1358,5 +1380,7 @@ namespace Umbraco.Core.Services.Implement
}
#endregion
}
}

View File

@@ -0,0 +1,97 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Umbraco.Core.Services;
namespace Umbraco.Web.HealthCheck.Checks.Data
{
[HealthCheck(
"73DD0C1C-E0CA-4C31-9564-1DCA509788AF",
"Database integrity check",
Description = "Checks for various data integrity issues in the Umbraco database.",
Group = "Data Integrity")]
public class DatabaseIntegrityCheck : HealthCheck
{
private readonly IContentService _contentService;
private readonly IMediaService _mediaService;
private const string _fixMediaPaths = "fixMediaPaths";
private const string _fixContentPaths = "fixContentPaths";
public DatabaseIntegrityCheck(IContentService contentService, IMediaService mediaService)
{
_contentService = contentService;
_mediaService = mediaService;
}
/// <summary>
/// Get the status for this health check
/// </summary>
/// <returns></returns>
public override IEnumerable<HealthCheckStatus> GetStatus()
{
//return the statuses
return new[]
{
CheckContent(),
CheckMedia()
};
}
private HealthCheckStatus CheckMedia()
{
return CheckPaths(_fixMediaPaths, "Fix media paths", "media", () =>
{
var mediaPaths = _mediaService.VerifyNodePaths(out var invalidMediaPaths);
return (mediaPaths, invalidMediaPaths);
});
}
private HealthCheckStatus CheckContent()
{
return CheckPaths(_fixContentPaths, "Fix content paths", "content", () =>
{
var contentPaths = _contentService.VerifyNodePaths(out var invalidContentPaths);
return (contentPaths, invalidContentPaths);
});
}
private HealthCheckStatus CheckPaths(string actionAlias, string actionName, string entityType, Func<(bool success, int[] invalidPaths)> doCheck)
{
var result = doCheck();
var actions = new List<HealthCheckAction>();
if (!result.success)
{
actions.Add(new HealthCheckAction(actionAlias, Id)
{
Name = actionName
});
}
return new HealthCheckStatus(result.success
? $"All {entityType} paths are valid"
: $"There are {result.invalidPaths.Length} invalid {entityType} paths")
{
ResultType = result.success ? StatusResultType.Success : StatusResultType.Error,
Actions = actions
};
}
public override HealthCheckStatus ExecuteAction(HealthCheckAction action)
{
switch (action.Alias)
{
case _fixContentPaths:
_contentService.FixNodePaths();
return CheckContent();
case _fixMediaPaths:
_mediaService.FixNodePaths();
return CheckMedia();
default:
throw new InvalidOperationException("Action not supported");
}
}
}
}

View File

@@ -156,6 +156,7 @@
<Compile Include="Editors\MacrosController.cs" />
<Compile Include="Editors\RelationTypeController.cs" />
<Compile Include="Editors\TinyMceController.cs" />
<Compile Include="HealthCheck\Checks\Data\DatabaseIntegrityCheck.cs" />
<Compile Include="ImageCropperTemplateCoreExtensions.cs" />
<Compile Include="IUmbracoContextFactory.cs" />
<Compile Include="Install\ChangesMonitor.cs" />