Merge pull request #17794 from umbraco/v15/bugfix/corrupt-examine-index-dashboard

Make sure Examine dashboard still functions when an index is corrupt
This commit is contained in:
Bjarke Berg
2024-12-12 11:54:34 +01:00
committed by GitHub
9 changed files with 134 additions and 21 deletions

View File

@@ -1,6 +1,9 @@
using Examine;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Api.Management.ViewModels.Indexer;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Infrastructure.Examine;
using Umbraco.Cms.Infrastructure.Services;
@@ -11,16 +14,34 @@ public class IndexPresentationFactory : IIndexPresentationFactory
private readonly IIndexDiagnosticsFactory _indexDiagnosticsFactory;
private readonly IIndexRebuilder _indexRebuilder;
private readonly IIndexingRebuilderService _indexingRebuilderService;
private readonly ILogger<IndexPresentationFactory> _logger;
public IndexPresentationFactory(IIndexDiagnosticsFactory indexDiagnosticsFactory, IIndexRebuilder indexRebuilder, IIndexingRebuilderService indexingRebuilderService)
public IndexPresentationFactory(
IIndexDiagnosticsFactory indexDiagnosticsFactory,
IIndexRebuilder indexRebuilder,
IIndexingRebuilderService indexingRebuilderService,
ILogger<IndexPresentationFactory> logger)
{
_indexDiagnosticsFactory = indexDiagnosticsFactory;
_indexRebuilder = indexRebuilder;
_indexingRebuilderService = indexingRebuilderService;
_logger = logger;
}
[Obsolete("Use the non obsolete method instead. Scheduled for removal in v17")]
public IndexPresentationFactory(IIndexDiagnosticsFactory indexDiagnosticsFactory, IIndexRebuilder indexRebuilder, IIndexingRebuilderService indexingRebuilderService)
:this(
indexDiagnosticsFactory,
indexRebuilder,
indexingRebuilderService,
StaticServiceProvider.Instance.GetRequiredService<ILogger<IndexPresentationFactory>>())
{
}
public IndexResponseModel Create(IIndex index)
{
var isCorrupt = !TryGetSearcherName(index, out var searcherName);
if (_indexingRebuilderService.IsRebuilding(index.Name))
{
return new IndexResponseModel
@@ -28,9 +49,9 @@ public class IndexPresentationFactory : IIndexPresentationFactory
Name = index.Name,
HealthStatus = new HealthStatusResponseModel
{
Status = HealthStatus.Rebuilding,
Status = isCorrupt ? HealthStatus.Corrupt : HealthStatus.Rebuilding,
},
SearcherName = index.Searcher.Name,
SearcherName = searcherName,
DocumentCount = 0,
FieldCount = 0,
};
@@ -55,21 +76,78 @@ public class IndexPresentationFactory : IIndexPresentationFactory
}
}
if (TryGetDocumentCount(indexDiag, index, out var documentCount) is false)
{
isCorrupt = true;
}
if (TryGetFieldNameCount(indexDiag, index, out var fieldNameCount) is false)
{
isCorrupt = true;
}
var indexerModel = new IndexResponseModel
{
Name = index.Name,
HealthStatus = new HealthStatusResponseModel
{
Status = isHealthyAttempt.Success ? HealthStatus.Healthy : HealthStatus.Unhealthy,
Status = isCorrupt
? HealthStatus.Corrupt
: isHealthyAttempt.Success ? HealthStatus.Healthy : HealthStatus.Unhealthy,
Message = isHealthyAttempt.Result,
},
CanRebuild = _indexRebuilder.CanRebuild(index.Name),
SearcherName = index.Searcher.Name,
DocumentCount = indexDiag.GetDocumentCount(),
FieldCount = indexDiag.GetFieldNames().Count(),
CanRebuild = isCorrupt is false && _indexRebuilder.CanRebuild(index.Name),
SearcherName = searcherName,
DocumentCount = documentCount,
FieldCount = fieldNameCount,
ProviderProperties = properties,
};
return indexerModel;
}
private bool TryGetSearcherName(IIndex index, out string name)
{
try
{
name = index.Searcher.Name;
return true;
}
catch (Exception e)
{
_logger.LogError(e, "An error occured trying to get the searcher name of index {IndexName}", index.Name);
name = "Could not determine searcher name because of error.";
return false;
}
}
private bool TryGetDocumentCount(IIndexDiagnostics indexDiag, IIndex index, out long documentCount)
{
try
{
documentCount = indexDiag.GetDocumentCount();
return true;
}
catch (Exception e)
{
_logger.LogError(e, "An error occured trying to get the document count of index {IndexName}", index.Name);
documentCount = 0;
return false;
}
}
private bool TryGetFieldNameCount(IIndexDiagnostics indexDiag, IIndex index, out int fieldNameCount)
{
try
{
fieldNameCount = indexDiag.GetFieldNames().Count();
return true;
}
catch (Exception e)
{
_logger.LogError(e, "An error occured trying to get the field name count of index {IndexName}", index.Name);
fieldNameCount = 0;
return false;
}
}
}

View File

@@ -38339,6 +38339,7 @@
"enum": [
"Healthy",
"Unhealthy",
"Corrupt",
"Rebuilding"
],
"type": "string"
@@ -40758,7 +40759,8 @@
"format": "uuid"
},
"packagePath": {
"type": "string"
"type": "string",
"readOnly": true
}
},
"additionalProperties": false
@@ -42742,22 +42744,26 @@
{
"$ref": "#/components/schemas/RelationReferenceModel"
}
]
],
"readOnly": true
},
"child": {
"oneOf": [
{
"$ref": "#/components/schemas/RelationReferenceModel"
}
]
],
"readOnly": true
},
"createDate": {
"type": "string",
"format": "date-time"
"format": "date-time",
"readOnly": true
},
"comment": {
"type": "string",
"nullable": true
"nullable": true,
"readOnly": true
}
},
"additionalProperties": false
@@ -44939,7 +44945,8 @@
}
},
"packagePath": {
"type": "string"
"type": "string",
"readOnly": true
}
},
"additionalProperties": false
@@ -46093,4 +46100,4 @@
}
}
}
}
}

View File

@@ -4,5 +4,6 @@ public enum HealthStatus
{
Healthy,
Unhealthy,
Rebuilding
Rebuilding,
Corrupt
}

View File

@@ -187,8 +187,7 @@ public class ExamineIndexRebuilder : IIndexRebuilder
{
// If an index exists but it has zero docs we'll consider it empty and rebuild
IIndex[] indexes = (onlyEmptyIndexes
? _examineManager.Indexes.Where(x =>
!x.IndexExists() || (x is IIndexStats stats && stats.GetDocumentCount() == 0))
? _examineManager.Indexes.Where(x => ShouldRebuild(x))
: _examineManager.Indexes).ToArray();
if (indexes.Length == 0)
@@ -228,4 +227,17 @@ public class ExamineIndexRebuilder : IIndexRebuilder
}
}
}
private bool ShouldRebuild(IIndex index)
{
try
{
return !index.IndexExists() || (index is IIndexStats stats && stats.GetDocumentCount() == 0);
}
catch (Exception e)
{
_logger.LogError(e, "An error occured trying to get determine index shouldRebuild status for index {IndexName}. The index will NOT be considered for rebuilding", index.Name);
return false;
}
}
}

View File

@@ -653,6 +653,8 @@ export default {
'The process is taking longer than expected, check the Umbraco log to see if there have been any errors during this operation',
indexCannotRebuild: 'This index cannot be rebuilt because it has no assigned',
iIndexPopulator: 'IIndexPopulator',
corruptStatus: 'Possible corrupt index detected',
corruptErrorDescription: 'Error received when evaluating the index:'
},
placeholders: {
username: 'Enter your username',

View File

@@ -1027,6 +1027,7 @@ export type HealthCheckWithResultPresentationModel = {
export enum HealthStatusModel {
HEALTHY = 'Healthy',
UNHEALTHY = 'Unhealthy',
CORRUPT = 'Corrupt',
REBUILDING = 'Rebuilding'
}

View File

@@ -93,8 +93,12 @@ export class UmbDashboardExamineIndexElement extends UmbLitElement {
switch (healthStatus.status) {
case HealthStatusModel.HEALTHY:
return html`<umb-icon name="icon-check color-green"></umb-icon>${msg}`;
case HealthStatusModel.CORRUPT:
return html`<umb-icon name="icon-alert color-red"></umb-icon><div>
<a href="https://umbra.co/corrupt-indexes" target="_blank"><umb-localize key="examineManagement_corruptStatus">Possible corrupt index detected</umb-localize></a>
<p><umb-localize key="examineManagement_corruptErrorDescription">Error received when evaluating the index:</umb-localize> </br> </p>${msg}</div>`;
case HealthStatusModel.UNHEALTHY:
return html`<umb-icon name="icon-error color-red"></umb-icon>${msg}`;
return html`<umb-icon name="icon-alert color-red"></umb-icon>${msg}`;
case HealthStatusModel.REBUILDING:
return html`<umb-icon name="icon-time color-yellow"></umb-icon>${msg}`;
default:
@@ -174,9 +178,14 @@ export class UmbDashboardExamineIndexElement extends UmbLitElement {
css`
#health-status {
display: flex;
align-items: start;
gap: var(--uui-size-6);
}
#health-status umb-icon {
margin-top: var(--uui-size-1);
}
:host {
display: block;
}

View File

@@ -44,8 +44,9 @@ export class UmbDashboardExamineOverviewElement extends UmbLitElement {
switch (status) {
case HealthStatusModel.HEALTHY:
return html`<umb-icon name="icon-check color-green"></umb-icon>`;
case HealthStatusModel.CORRUPT:
case HealthStatusModel.UNHEALTHY:
return html`<umb-icon name="icon-error color-red"></umb-icon>`;
return html`<umb-icon name="icon-alert color-red"></umb-icon>`;
case HealthStatusModel.REBUILDING:
return html`<umb-icon name="icon-time color-yellow"></umb-icon>`;
default:

View File

@@ -1,5 +1,6 @@
using System.Collections.ObjectModel;
using Examine;
using Microsoft.Extensions.Logging;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Api.Management.Factories;
@@ -51,7 +52,8 @@ public class IndexPresentationFactoryTests
var factory = new IndexPresentationFactory(
indexDiagnosticsFactoryMock.Object,
indexRebuilderMock.Object,
indexRebuilderServiceMock.Object);
indexRebuilderServiceMock.Object,
Mock.Of<ILogger<IndexPresentationFactory>>());
// act