Merge pull request #10182 from umbraco/v8/bugfix/8893-examine-startup

Fix startup issues and timing with Examine and Nucache
This commit is contained in:
Shannon Deminick
2021-06-05 02:37:33 +10:00
committed by GitHub
19 changed files with 406 additions and 251 deletions

View File

@@ -28,7 +28,7 @@ namespace Umbraco.Core.Sync
// but only processes instructions coming from remote servers,
// thus ensuring that instructions run only once
//
public class DatabaseServerMessenger : ServerMessengerBase
public class DatabaseServerMessenger : ServerMessengerBase, ISyncBootStateAccessor
{
private readonly IRuntimeState _runtime;
private readonly ManualResetEvent _syncIdle;
@@ -39,9 +39,9 @@ namespace Umbraco.Core.Sync
private int _lastId = -1;
private DateTime _lastSync;
private DateTime _lastPruned;
private bool _initialized;
private bool _syncing;
private bool _released;
private readonly Lazy<SyncBootState> _getSyncBootState;
public DatabaseServerMessengerOptions Options { get; }
@@ -59,6 +59,7 @@ namespace Umbraco.Core.Sync
_lastPruned = _lastSync = DateTime.UtcNow;
_syncIdle = new ManualResetEvent(true);
_distCacheFilePath = new Lazy<string>(() => GetDistCacheFilePath(globalSettings));
_getSyncBootState = new Lazy<SyncBootState>(BootInternal);
}
protected ILogger Logger { get; }
@@ -75,7 +76,7 @@ namespace Umbraco.Core.Sync
{
// we don't care if there's servers listed or not,
// if distributed call is enabled we will make the call
return _initialized && DistributedEnabled;
return _getSyncBootState.IsValueCreated && DistributedEnabled;
}
protected override void DeliverRemote(
@@ -110,14 +111,14 @@ namespace Umbraco.Core.Sync
#region Sync
/// <summary>
/// Boots the messenger.
/// </summary>
/// <remarks>
/// Thread safety: this is NOT thread safe. Because it is NOT meant to run multi-threaded.
/// Callers MUST ensure thread-safety.
/// </remarks>
[Obsolete("This is no longer used and will be removed in future versions")]
protected void Boot()
{
// if called, just forces the boot logic
_ = GetSyncBootState();
}
private SyncBootState BootInternal()
{
// weight:10, must release *before* the published snapshot service, because once released
// the service will *not* be able to properly handle our notifications anymore
@@ -139,7 +140,7 @@ namespace Umbraco.Core.Sync
// properly releasing MainDom - a timeout here means that one refresher
// is taking too much time processing, however when it's done we will
// not update lastId and stop everything
var idle =_syncIdle.WaitOne(5000);
var idle = _syncIdle.WaitOne(5000);
if (idle == false)
{
Logger.Warn<DatabaseServerMessenger>("The wait lock timed out, application is shutting down. The current instruction batch will be re-processed.");
@@ -147,17 +148,23 @@ namespace Umbraco.Core.Sync
},
weight);
SyncBootState bootState = SyncBootState.Unknown;
if (registered == false)
return;
{
return bootState;
}
ReadLastSynced(); // get _lastId
using (var scope = ScopeProvider.CreateScope())
{
EnsureInstructions(scope.Database); // reset _lastId if instructions are missing
Initialize(scope.Database); // boot
bootState = Initialize(scope.Database); // boot
scope.Complete();
}
return bootState;
}
/// <summary>
@@ -167,60 +174,62 @@ namespace Umbraco.Core.Sync
/// Thread safety: this is NOT thread safe. Because it is NOT meant to run multi-threaded.
/// Callers MUST ensure thread-safety.
/// </remarks>
private void Initialize(IUmbracoDatabase database)
private SyncBootState Initialize(IUmbracoDatabase database)
{
lock (_locko)
{
if (_released) return;
// could occur if shutting down immediately once starting up and before we've initialized
if (_released) return SyncBootState.Unknown;
var coldboot = false;
if (_lastId < 0) // never synced before
var coldboot = false;
if (_lastId < 0) // never synced before
{
// we haven't synced - in this case we aren't going to sync the whole thing, we will assume this is a new
// server and it will need to rebuild it's own caches, eg Lucene or the xml cache file.
Logger.Warn<DatabaseServerMessenger>("No last synced Id found, this generally means this is a new server/install."
+ " The server will build its caches and indexes, and then adjust its last synced Id to the latest found in"
+ " the database and maintain cache updates based on that Id.");
coldboot = true;
}
else
{
//check for how many instructions there are to process, each row contains a count of the number of instructions contained in each
//row so we will sum these numbers to get the actual count.
var count = database.ExecuteScalar<int>("SELECT SUM(instructionCount) FROM umbracoCacheInstruction WHERE id > @lastId", new { lastId = _lastId });
if (count > Options.MaxProcessingInstructionCount)
{
// we haven't synced - in this case we aren't going to sync the whole thing, we will assume this is a new
// server and it will need to rebuild it's own caches, eg Lucene or the xml cache file.
Logger.Warn<DatabaseServerMessenger>("No last synced Id found, this generally means this is a new server/install."
+ " The server will build its caches and indexes, and then adjust its last synced Id to the latest found in"
+ " the database and maintain cache updates based on that Id.");
//too many instructions, proceed to cold boot
Logger.Warn<DatabaseServerMessenger, int, int>(
"The instruction count ({InstructionCount}) exceeds the specified MaxProcessingInstructionCount ({MaxProcessingInstructionCount})."
+ " The server will skip existing instructions, rebuild its caches and indexes entirely, adjust its last synced Id"
+ " to the latest found in the database and maintain cache updates based on that Id.",
count, Options.MaxProcessingInstructionCount);
coldboot = true;
}
else
{
//check for how many instructions there are to process, each row contains a count of the number of instructions contained in each
//row so we will sum these numbers to get the actual count.
var count = database.ExecuteScalar<int>("SELECT SUM(instructionCount) FROM umbracoCacheInstruction WHERE id > @lastId", new {lastId = _lastId});
if (count > Options.MaxProcessingInstructionCount)
{
//too many instructions, proceed to cold boot
Logger.Warn<DatabaseServerMessenger,int,int>(
"The instruction count ({InstructionCount}) exceeds the specified MaxProcessingInstructionCount ({MaxProcessingInstructionCount})."
+ " The server will skip existing instructions, rebuild its caches and indexes entirely, adjust its last synced Id"
+ " to the latest found in the database and maintain cache updates based on that Id.",
count, Options.MaxProcessingInstructionCount);
}
coldboot = true;
if (coldboot)
{
// go get the last id in the db and store it
// note: do it BEFORE initializing otherwise some instructions might get lost
// when doing it before, some instructions might run twice - not an issue
var maxId = database.ExecuteScalar<int>("SELECT MAX(id) FROM umbracoCacheInstruction");
//if there is a max currently, or if we've never synced
if (maxId > 0 || _lastId < 0)
SaveLastSynced(maxId);
// execute initializing callbacks
if (Options.InitializingCallbacks != null)
{
foreach (var callback in Options.InitializingCallbacks)
{
callback();
}
}
if (coldboot)
{
// go get the last id in the db and store it
// note: do it BEFORE initializing otherwise some instructions might get lost
// when doing it before, some instructions might run twice - not an issue
var maxId = database.ExecuteScalar<int>("SELECT MAX(id) FROM umbracoCacheInstruction");
//if there is a max currently, or if we've never synced
if (maxId > 0 || _lastId < 0)
SaveLastSynced(maxId);
// execute initializing callbacks
if (Options.InitializingCallbacks != null)
foreach (var callback in Options.InitializingCallbacks)
callback();
}
_initialized = true;
}
return coldboot ? SyncBootState.ColdBoot : SyncBootState.WarmBoot;
}
/// <summary>
@@ -352,7 +361,7 @@ namespace Umbraco.Core.Sync
}
catch (JsonException ex)
{
Logger.Error<DatabaseServerMessenger,int, string>(ex, "Failed to deserialize instructions ({DtoId}: '{DtoInstructions}').",
Logger.Error<DatabaseServerMessenger, int, string>(ex, "Failed to deserialize instructions ({DtoId}: '{DtoInstructions}').",
dto.Id,
dto.Instructions);
@@ -410,11 +419,11 @@ namespace Umbraco.Core.Sync
//}
catch (Exception ex)
{
Logger.Error<DatabaseServerMessenger,int, string> (
ex,
"DISTRIBUTED CACHE IS NOT UPDATED. Failed to execute instructions ({DtoId}: '{DtoInstructions}'). Instruction is being skipped/ignored",
dto.Id,
dto.Instructions);
Logger.Error<DatabaseServerMessenger, int, string>(
ex,
"DISTRIBUTED CACHE IS NOT UPDATED. Failed to execute instructions ({DtoId}: '{DtoInstructions}'). Instruction is being skipped/ignored",
dto.Id,
dto.Instructions);
//we cannot throw here because this invalid instruction will just keep getting processed over and over and errors
// will be thrown over and over. The only thing we can do is ignore and move on.
@@ -548,6 +557,8 @@ namespace Umbraco.Core.Sync
#endregion
public virtual SyncBootState GetSyncBootState() => _getSyncBootState.Value;
#region Notify refreshers
private static ICacheRefresher GetRefresher(Guid id)

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
namespace Umbraco.Core.Sync
{
@@ -24,13 +25,8 @@ namespace Umbraco.Core.Sync
/// </summary>
public int MaxProcessingInstructionCount { get; set; }
/// <summary>
/// A list of callbacks that will be invoked if the lastsynced.txt file does not exist.
/// </summary>
/// <remarks>
/// These callbacks will typically be for eg rebuilding the xml cache file, or examine indexes, based on
/// the data in the database to get this particular server node up to date.
/// </remarks>
[Obsolete("This should not be used. If initialization calls need to be invoked on a cold boot, use the ISyncBootStateAccessor.Booting event.")]
[EditorBrowsable(EditorBrowsableState.Never)]
public IEnumerable<Action> InitializingCallbacks { get; set; }
/// <summary>

View File

@@ -0,0 +1,16 @@
using System;
namespace Umbraco.Core.Sync
{
/// <summary>
/// Retrieve the state of the sync service
/// </summary>
public interface ISyncBootStateAccessor
{
/// <summary>
/// Get the boot state
/// </summary>
/// <returns></returns>
SyncBootState GetSyncBootState();
}
}

View File

@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Umbraco.Core.Sync
{
/// <summary>
/// Boot state implementation for when umbraco is not in the run state
/// </summary>
public class NonRuntimeLevelBootStateAccessor : ISyncBootStateAccessor
{
public event EventHandler<SyncBootState> Booting;
public SyncBootState GetSyncBootState()
{
return SyncBootState.Unknown;
}
}
}

View File

@@ -0,0 +1,20 @@
namespace Umbraco.Core.Sync
{
public enum SyncBootState
{
/// <summary>
/// Unknown state. Treat as WarmBoot
/// </summary>
Unknown = 0,
/// <summary>
/// Cold boot. No Sync state
/// </summary>
ColdBoot = 1,
/// <summary>
/// Warm boot. Sync state present
/// </summary>
WarmBoot = 2
}
}

View File

@@ -180,6 +180,9 @@
<Compile Include="Migrations\Upgrade\V_8_7_0\MissingDictionaryIndex.cs" />
<Compile Include="Services\IInstallationService.cs" />
<Compile Include="Services\IUpgradeService.cs" />
<Compile Include="Sync\ISyncBootStateAccessor.cs" />
<Compile Include="Sync\NonRuntimeLevelBootStateAccessor.cs" />
<Compile Include="Sync\SyncBootState.cs" />
<Compile Include="SystemLock.cs" />
<Compile Include="Attempt.cs" />
<Compile Include="AttemptOfTResult.cs" />