Files
Umbraco-CMS/src/Umbraco.Web/ExamineStartup.cs
2018-01-08 18:10:27 +11:00

208 lines
9.9 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using Examine;
using Examine.Config;
using Examine.LuceneEngine;
using Examine.LuceneEngine.Providers;
using Examine.Providers;
using Lucene.Net.Index;
using Lucene.Net.Store;
using Umbraco.Core;
using Umbraco.Core.Logging;
using UmbracoExamine;
namespace Umbraco.Web
{
/// <summary>
/// Used to configure Examine during startup for the web application
/// </summary>
internal class ExamineStartup
{
private readonly List<BaseIndexProvider> _indexesToRebuild = new List<BaseIndexProvider>();
private readonly ApplicationContext _appCtx;
private readonly ProfilingLogger _profilingLogger;
private static bool _isConfigured = false;
//this is used if we are not the MainDom, in which case we need to ensure that if indexes need rebuilding that this
//doesn't occur since that should only occur when we are MainDom
private bool _disableExamineIndexing = false;
public ExamineStartup(ApplicationContext appCtx)
{
_appCtx = appCtx;
_profilingLogger = appCtx.ProfilingLogger;
}
/// <summary>
/// Called during the initialize operation of the boot manager process
/// </summary>
public void Initialize()
{
//We want to manage Examine's appdomain shutdown sequence ourselves so first we'll disable Examine's default behavior
//and then we'll use MainDom to control Examine's shutdown
ExamineManager.DisableDefaultHostingEnvironmentRegistration();
//we want to tell examine to use a different fs lock instead of the default NativeFSFileLock which could cause problems if the appdomain
//terminates and in some rare cases would only allow unlocking of the file if IIS is forcefully terminated. Instead we'll rely on the simplefslock
//which simply checks the existence of the lock file
DirectoryTracker.DefaultLockFactory = d =>
{
var simpleFsLockFactory = new NoPrefixSimpleFsLockFactory(d);
return simpleFsLockFactory;
};
//This is basically a hack for this item: http://issues.umbraco.org/issue/U4-5976
// when Examine initializes it will try to rebuild if the indexes are empty, however in many cases not all of Examine's
// event handlers will be assigned during bootup when the rebuilding starts which is a problem. So with the examine 0.1.58.2941 build
// it has an event we can subscribe to in order to cancel this rebuilding process, but what we'll do is cancel it and postpone the rebuilding until the
// boot process has completed. It's a hack but it works.
ExamineManager.Instance.BuildingEmptyIndexOnStartup += OnInstanceOnBuildingEmptyIndexOnStartup;
//let's deal with shutting down Examine with MainDom
var examineShutdownRegistered = _appCtx.MainDom.Register(() =>
{
using (_profilingLogger.TraceDuration<ExamineStartup>("Examine shutting down"))
{
//Due to the way Examine's own IRegisteredObject works, we'll first run it with immediate=false and then true so that
//it's correct subroutines are executed (otherwise we'd have to run this logic manually ourselves)
ExamineManager.Instance.Stop(false);
ExamineManager.Instance.Stop(true);
}
});
if (examineShutdownRegistered)
{
_profilingLogger.Logger.Debug<ExamineStartup>("Examine shutdown registered with MainDom");
}
else
{
_profilingLogger.Logger.Debug<ExamineStartup>("Examine shutdown not registered, this appdomain is not the MainDom");
//if we could not register the shutdown examine ourselves, it means we are not maindom! in this case all of examine should be disabled
//from indexing anything on startup!!
_disableExamineIndexing = true;
Suspendable.ExamineEvents.SuspendIndexers();
}
}
/// <summary>
/// Called during the Complete operation of the boot manager process
/// </summary>
public void Complete()
{
EnsureUnlockedAndConfigured();
//Ok, now that everything is complete we'll check if we've stored any references to index that need rebuilding and run them
// (see the initialize method for notes) - we'll ensure we remove the event handler too in case examine manager doesn't actually
// initialize during startup, in which case we want it to rebuild the indexes itself.
ExamineManager.Instance.BuildingEmptyIndexOnStartup -= OnInstanceOnBuildingEmptyIndexOnStartup;
//don't do anything if we have disabled this
if (_disableExamineIndexing == false)
{
foreach (var indexer in _indexesToRebuild)
{
indexer.RebuildIndex();
}
}
}
/// <summary>
/// Called to perform the rebuilding indexes on startup if the indexes don't exist
/// </summary>
public void RebuildIndexes()
{
//don't do anything if we have disabled this
if (_disableExamineIndexing) return;
EnsureUnlockedAndConfigured();
//If the developer has explicitly opted out of rebuilding indexes on startup then we
// should adhere to that and not do it, this means that if they are load balancing things will be
// out of sync if they are auto-scaling but there's not much we can do about that.
if (ExamineSettings.Instance.RebuildOnAppStart == false) return;
foreach (var indexer in GetIndexesForColdBoot())
{
indexer.RebuildIndex();
}
}
/// <summary>
/// The method used to create indexes on a cold boot
/// </summary>
/// <remarks>
/// A cold boot is when the server determines it will not (or cannot) process instructions in the cache table and
/// will rebuild it's own caches itself.
/// </remarks>
public IEnumerable<BaseIndexProvider> GetIndexesForColdBoot()
{
// NOTE: This is IMPORTANT! ... we don't want to rebuild any index that is already flagged to be re-indexed
// on startup based on our _indexesToRebuild variable and how Examine auto-rebuilds when indexes are empty.
// This callback is used above for the DatabaseServerMessenger startup options.
// all indexes
IEnumerable<BaseIndexProvider> indexes = ExamineManager.Instance.IndexProviderCollection;
// except those that are already flagged
// and are processed in Complete()
if (_indexesToRebuild.Any())
indexes = indexes.Except(_indexesToRebuild);
// return
foreach (var index in indexes)
yield return index;
}
/// <summary>
/// Must be called to configure each index and ensure it's unlocked before any indexing occurs
/// </summary>
/// <remarks>
/// Indexing rebuilding can occur on a normal boot if the indexes are empty or on a cold boot by the database server messenger. Before
/// either of these happens, we need to configure the indexes.
/// </remarks>
private void EnsureUnlockedAndConfigured()
{
if (_isConfigured) return;
_isConfigured = true;
foreach (var luceneIndexer in ExamineManager.Instance.IndexProviderCollection.OfType<LuceneIndexer>())
{
//We now need to disable waiting for indexing for Examine so that the appdomain is shutdown immediately and doesn't wait for pending
//indexing operations. We used to wait for indexing operations to complete but this can cause more problems than that is worth because
//that could end up halting shutdown for a very long time causing overlapping appdomains and many other problems.
luceneIndexer.WaitForIndexQueueOnShutdown = false;
//we should check if the index is locked ... it shouldn't be! We are using simple fs lock now and we are also ensuring that
//the indexes are not operational unless MainDom is true so if _disableExamineIndexing is false then we should be in charge
if (_disableExamineIndexing == false)
{
var dir = luceneIndexer.GetLuceneDirectory();
if (IndexWriter.IsLocked(dir))
{
_profilingLogger.Logger.Info<ExamineStartup>("Forcing index " + luceneIndexer.IndexSetName + " to be unlocked since it was left in a locked state");
IndexWriter.Unlock(dir);
}
}
}
}
private void OnInstanceOnBuildingEmptyIndexOnStartup(object sender, BuildingEmptyIndexOnStartupEventArgs args)
{
//store the indexer that needs rebuilding because it's empty for when the boot process
// is complete and cancel this current event so the rebuild process doesn't start right now.
args.Cancel = true;
_indexesToRebuild.Add((BaseIndexProvider)args.Indexer);
//check if the index is rebuilding due to an error and log it
if (args.IsHealthy == false)
{
var baseIndex = args.Indexer as BaseIndexProvider;
var name = baseIndex != null ? baseIndex.Name : "[UKNOWN]";
_profilingLogger.Logger.Error<ExamineStartup>(string.Format("The index {0} is rebuilding due to being unreadable/corrupt", name), args.UnhealthyException);
}
}
}
}