From 7d135899be68a4b79bfada012c2b438461772042 Mon Sep 17 00:00:00 2001 From: nzdev <834725+nzdev@users.noreply.github.com> Date: Sun, 21 Feb 2021 20:45:03 +1300 Subject: [PATCH 1/8] load only once --- .../Sync/DatabaseServerMessenger.cs | 90 ++++++++++++------- .../Sync/ISyncBootStateAccessor.cs | 35 ++++++++ src/Umbraco.Core/Umbraco.Core.csproj | 1 + .../PublishedContent/NuCacheChildrenTests.cs | 4 +- .../PublishedContent/NuCacheTests.cs | 4 +- .../Scoping/ScopedNuCacheTests.cs | 3 +- .../ContentTypeServiceVariantsTests.cs | 4 +- .../TestHelpers/TestSyncBootStateAccessor.cs | 23 +++++ src/Umbraco.Tests/Umbraco.Tests.csproj | 1 + ...aseServerRegistrarAndMessengerComponent.cs | 1 + .../NuCache/PublishedSnapshotService.cs | 17 +++- 11 files changed, 146 insertions(+), 37 deletions(-) create mode 100644 src/Umbraco.Core/Sync/ISyncBootStateAccessor.cs create mode 100644 src/Umbraco.Tests/TestHelpers/TestSyncBootStateAccessor.cs mode change 100755 => 100644 src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs diff --git a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs index 7442169b44..8264ddd79c 100644 --- a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs +++ b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs @@ -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; @@ -172,35 +172,7 @@ namespace Umbraco.Core.Sync lock (_locko) { if (_released) return; - - 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("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("SELECT SUM(instructionCount) FROM umbracoCacheInstruction WHERE id > @lastId", new {lastId = _lastId}); - if (count > Options.MaxProcessingInstructionCount) - { - //too many instructions, proceed to cold boot - Logger.Warn( - "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; - } - } + var coldboot = IsColdBoot(database); if (coldboot) { @@ -223,6 +195,40 @@ namespace Umbraco.Core.Sync } } + private bool IsColdBoot(IUmbracoDatabase database) + { + 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("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("SELECT SUM(instructionCount) FROM umbracoCacheInstruction WHERE id > @lastId", new {lastId = _lastId}); + if (count > Options.MaxProcessingInstructionCount) + { + //too many instructions, proceed to cold boot + Logger.Warn( + "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; + } + } + + return coldboot; + } + /// /// Synchronize the server (throttled). /// @@ -548,6 +554,30 @@ namespace Umbraco.Core.Sync #endregion + public SyncBootState GetSyncBootState() + { + try + { + ReadLastSynced(); // get _lastId + using (var scope = ScopeProvider.CreateScope()) + { + EnsureInstructions(scope.Database); + bool isColdBoot = IsColdBoot(scope.Database); + + if (isColdBoot) + { + return SyncBootState.ColdBoot; + } + return SyncBootState.HasSyncState; + } + } + catch(Exception ex) + { + Logger.Warn("Error determining Sync Boot State", ex); + return SyncBootState.Unknown; + } + } + #region Notify refreshers private static ICacheRefresher GetRefresher(Guid id) diff --git a/src/Umbraco.Core/Sync/ISyncBootStateAccessor.cs b/src/Umbraco.Core/Sync/ISyncBootStateAccessor.cs new file mode 100644 index 0000000000..a7b7c58235 --- /dev/null +++ b/src/Umbraco.Core/Sync/ISyncBootStateAccessor.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Umbraco.Core.Sync +{ + /// + /// Retrieve the state of the sync service + /// + public interface ISyncBootStateAccessor + { + /// + /// Get the boot state + /// + /// + SyncBootState GetSyncBootState(); + } + public enum SyncBootState + { + /// + /// Unknown state. Treat as HasSyncState + /// + Unknown = 0, + /// + /// Cold boot. No Sync state + /// + ColdBoot = 1, + /// + /// Warm boot. Sync state present + /// + HasSyncState = 2 + } +} diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 465ddee6ee..5c3dd074dd 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -173,6 +173,7 @@ + diff --git a/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs b/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs index 834d211994..068a161268 100644 --- a/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs +++ b/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs @@ -17,6 +17,7 @@ using Umbraco.Core.Scoping; using Umbraco.Core.Services; using Umbraco.Core.Services.Changes; using Umbraco.Core.Strings; +using Umbraco.Core.Sync; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.Testing.Objects; using Umbraco.Tests.Testing.Objects.Accessors; @@ -155,7 +156,8 @@ namespace Umbraco.Tests.PublishedContent globalSettings, Mock.Of(), Mock.Of(), - new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() })); + new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() }), + new TestSyncBootStateAccessor(SyncBootState.HasSyncState)); // invariant is the current default _variationAccesor.VariationContext = new VariationContext(); diff --git a/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs b/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs index 0e05e6baad..652891c476 100644 --- a/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs +++ b/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs @@ -17,6 +17,7 @@ using Umbraco.Core.Scoping; using Umbraco.Core.Services; using Umbraco.Core.Services.Changes; using Umbraco.Core.Strings; +using Umbraco.Core.Sync; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.Testing.Objects; using Umbraco.Tests.Testing.Objects.Accessors; @@ -201,7 +202,8 @@ namespace Umbraco.Tests.PublishedContent globalSettings, Mock.Of(), Mock.Of(), - new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() })); + new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() }), + new TestSyncBootStateAccessor(SyncBootState.HasSyncState)); // invariant is the current default _variationAccesor.VariationContext = new VariationContext(); diff --git a/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs b/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs index c7c403b260..e204bd7b5f 100644 --- a/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs +++ b/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs @@ -99,7 +99,8 @@ namespace Umbraco.Tests.Scoping Factory.GetInstance(), Factory.GetInstance(), Mock.Of(), - new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() })); + new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() }), + new TestSyncBootStateAccessor(SyncBootState.HasSyncState)); } protected UmbracoContext GetUmbracoContextNu(string url, int templateId = 1234, RouteData routeData = null, bool setSingleton = false, IUmbracoSettingsSection umbracoSettings = null, IEnumerable urlProviders = null) diff --git a/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs index 9391b7442f..9cd4bb63e8 100644 --- a/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs +++ b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs @@ -17,6 +17,7 @@ using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; using Umbraco.Core.Strings; using Umbraco.Core.Sync; +using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; using Umbraco.Tests.Testing; using Umbraco.Web.PublishedCache; @@ -70,7 +71,8 @@ namespace Umbraco.Tests.Services Factory.GetInstance(), Factory.GetInstance(), Mock.Of(), - new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() })); + new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() }), + new TestSyncBootStateAccessor(SyncBootState.HasSyncState)); } public class LocalServerMessenger : ServerMessengerBase diff --git a/src/Umbraco.Tests/TestHelpers/TestSyncBootStateAccessor.cs b/src/Umbraco.Tests/TestHelpers/TestSyncBootStateAccessor.cs new file mode 100644 index 0000000000..e5f6989381 --- /dev/null +++ b/src/Umbraco.Tests/TestHelpers/TestSyncBootStateAccessor.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core.Sync; + +namespace Umbraco.Tests.TestHelpers +{ + class TestSyncBootStateAccessor : ISyncBootStateAccessor + { + private readonly SyncBootState _syncBootState; + + public TestSyncBootStateAccessor(SyncBootState syncBootState) + { + _syncBootState = syncBootState; + } + public SyncBootState GetSyncBootState() + { + return _syncBootState; + } + } +} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 2ac28aa7d7..6c4f7415ea 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -175,6 +175,7 @@ + diff --git a/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs b/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs index 2fa9d80779..688fc268b0 100644 --- a/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs +++ b/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs @@ -72,6 +72,7 @@ namespace Umbraco.Web.Compose composition.SetDatabaseServerMessengerOptions(GetDefaultOptions); composition.SetServerMessenger(); + composition.Register(Lifetime.Singleton); } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs old mode 100755 new mode 100644 index a39e26e2b1..a592fed4be --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -24,6 +24,7 @@ using Umbraco.Core.Services; using Umbraco.Core.Services.Changes; using Umbraco.Core.Services.Implement; using Umbraco.Core.Strings; +using Umbraco.Core.Sync; using Umbraco.Web.Cache; using Umbraco.Web.Install; using Umbraco.Web.PublishedCache.NuCache.DataSource; @@ -63,6 +64,8 @@ namespace Umbraco.Web.PublishedCache.NuCache private bool _localContentDbExists; private bool _localMediaDbExists; + private readonly ISyncBootStateAccessor _syncBootStateAccessor; + // define constant - determines whether to use cache when previewing // to store eg routes, property converted values, anything - caching // means faster execution, but uses memory - not sure if we want it @@ -81,7 +84,8 @@ namespace Umbraco.Web.PublishedCache.NuCache IDataSource dataSource, IGlobalSettings globalSettings, IEntityXmlSerializer entitySerializer, IPublishedModelFactory publishedModelFactory, - UrlSegmentProviderCollection urlSegmentProviders) + UrlSegmentProviderCollection urlSegmentProviders, + ISyncBootStateAccessor syncBootStateAccessor) : base(publishedSnapshotAccessor, variationContextAccessor) { //if (Interlocked.Increment(ref _singletonCheck) > 1) @@ -99,6 +103,8 @@ namespace Umbraco.Web.PublishedCache.NuCache _globalSettings = globalSettings; _urlSegmentProviders = urlSegmentProviders; + _syncBootStateAccessor = syncBootStateAccessor; + // we need an Xml serializer here so that the member cache can support XPath, // for members this is done by navigating the serialized-to-xml member _entitySerializer = entitySerializer; @@ -217,7 +223,12 @@ namespace Umbraco.Web.PublishedCache.NuCache { var okContent = false; var okMedia = false; - + if (_syncBootStateAccessor.GetSyncBootState() == SyncBootState.ColdBoot) + { + _logger.Warn("Sync Service is in a Cold Boot state. Skip LoadCachesOnStartup as the Sync Service will trigger a full reload"); + _isReady = true; + return; + } try { if (_localContentDbExists) @@ -233,7 +244,7 @@ namespace Umbraco.Web.PublishedCache.NuCache if (!okMedia) _logger.Warn("Loading media from local db raised warnings, will reload from database."); } - + if (!okContent) LockAndLoadContent(scope => LoadContentFromDatabaseLocked(scope, true)); From 5f9d126ab7d087a204e32aed78148b07bc12ea2b Mon Sep 17 00:00:00 2001 From: nzdev <834725+nzdev@users.noreply.github.com> Date: Fri, 26 Mar 2021 16:13:47 +1300 Subject: [PATCH 2/8] fix support for non run states --- .../Sync/ISyncBootStateAccessor.cs | 15 ------------ .../Sync/NonRuntimeLevelBootStateAccessor.cs | 19 +++++++++++++++ src/Umbraco.Core/Sync/SyncBootState.cs | 24 +++++++++++++++++++ src/Umbraco.Core/Umbraco.Core.csproj | 2 ++ ...aseServerRegistrarAndMessengerComponent.cs | 2 +- .../PublishedCache/NuCache/NuCacheComposer.cs | 4 ++++ 6 files changed, 50 insertions(+), 16 deletions(-) create mode 100644 src/Umbraco.Core/Sync/NonRuntimeLevelBootStateAccessor.cs create mode 100644 src/Umbraco.Core/Sync/SyncBootState.cs diff --git a/src/Umbraco.Core/Sync/ISyncBootStateAccessor.cs b/src/Umbraco.Core/Sync/ISyncBootStateAccessor.cs index a7b7c58235..4b8500f2d9 100644 --- a/src/Umbraco.Core/Sync/ISyncBootStateAccessor.cs +++ b/src/Umbraco.Core/Sync/ISyncBootStateAccessor.cs @@ -17,19 +17,4 @@ namespace Umbraco.Core.Sync /// SyncBootState GetSyncBootState(); } - public enum SyncBootState - { - /// - /// Unknown state. Treat as HasSyncState - /// - Unknown = 0, - /// - /// Cold boot. No Sync state - /// - ColdBoot = 1, - /// - /// Warm boot. Sync state present - /// - HasSyncState = 2 - } } diff --git a/src/Umbraco.Core/Sync/NonRuntimeLevelBootStateAccessor.cs b/src/Umbraco.Core/Sync/NonRuntimeLevelBootStateAccessor.cs new file mode 100644 index 0000000000..70cec6cc96 --- /dev/null +++ b/src/Umbraco.Core/Sync/NonRuntimeLevelBootStateAccessor.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Umbraco.Core.Sync +{ + /// + /// Boot state implementation for when umbraco is not in the run state + /// + public class NonRuntimeLevelBootStateAccessor : ISyncBootStateAccessor + { + public SyncBootState GetSyncBootState() + { + return SyncBootState.Unknown; + } + } +} diff --git a/src/Umbraco.Core/Sync/SyncBootState.cs b/src/Umbraco.Core/Sync/SyncBootState.cs new file mode 100644 index 0000000000..4abc53abba --- /dev/null +++ b/src/Umbraco.Core/Sync/SyncBootState.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Umbraco.Core.Sync +{ + public enum SyncBootState + { + /// + /// Unknown state. Treat as HasSyncState + /// + Unknown = 0, + /// + /// Cold boot. No Sync state + /// + ColdBoot = 1, + /// + /// Warm boot. Sync state present + /// + HasSyncState = 2 + } +} diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 5c3dd074dd..e0c0d78112 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -174,6 +174,8 @@ + + diff --git a/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs b/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs index 688fc268b0..26ba0db324 100644 --- a/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs +++ b/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs @@ -72,7 +72,7 @@ namespace Umbraco.Web.Compose composition.SetDatabaseServerMessengerOptions(GetDefaultOptions); composition.SetServerMessenger(); - composition.Register(Lifetime.Singleton); + composition.Register(factory=> factory.GetInstance() as BatchedDatabaseServerMessenger, Lifetime.Singleton); } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs index f748fd555c..99f2786d49 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs @@ -1,5 +1,6 @@ using Umbraco.Core; using Umbraco.Core.Composing; +using Umbraco.Core.Sync; using Umbraco.Web.PublishedCache.NuCache.DataSource; namespace Umbraco.Web.PublishedCache.NuCache @@ -10,6 +11,9 @@ namespace Umbraco.Web.PublishedCache.NuCache { base.Compose(composition); + //Overriden on Run state in DatabaseServerRegistrarAndMessengerComposer + composition.Register(Lifetime.Singleton); + // register the NuCache database data source composition.Register(); From 306c82f871a0346b04322b604ceaf569a3861fbd Mon Sep 17 00:00:00 2001 From: nzdev <834725+nzdev@users.noreply.github.com> Date: Fri, 23 Apr 2021 17:03:32 +1200 Subject: [PATCH 3/8] Change log to Info --- .../PublishedCache/NuCache/PublishedSnapshotService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index a592fed4be..58892057d2 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -225,7 +225,7 @@ namespace Umbraco.Web.PublishedCache.NuCache var okMedia = false; if (_syncBootStateAccessor.GetSyncBootState() == SyncBootState.ColdBoot) { - _logger.Warn("Sync Service is in a Cold Boot state. Skip LoadCachesOnStartup as the Sync Service will trigger a full reload"); + _logger.Info("Sync Service is in a Cold Boot state. Skip LoadCachesOnStartup as the Sync Service will trigger a full reload"); _isReady = true; return; } From 053a56a45bc8d6b9917513a84d9953039790f31a Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 26 Apr 2021 18:07:23 +1000 Subject: [PATCH 4/8] Removes the callbacks from DatabaseServerMessenger, moves logic for cold boot into the components responsible for what needs to happen in cold boot. This is still not ideal but we are stuck with backwards compat. In netcore the initialization will be fixed up (if not already). Ensure examine rebuilds don't occur until the first http request is done instead of on a timer which could be problematic with cold boots. --- .../Sync/DatabaseServerMessenger.cs | 143 ++++++++---------- .../Sync/DatabaseServerMessengerOptions.cs | 10 +- .../Sync/ISyncBootStateAccessor.cs | 9 +- .../Sync/NonRuntimeLevelBootStateAccessor.cs | 2 + src/Umbraco.Core/Sync/SyncBootState.cs | 14 +- .../PublishedContent/NuCacheChildrenTests.cs | 2 +- .../PublishedContent/NuCacheTests.cs | 2 +- .../Scoping/ScopedNuCacheTests.cs | 2 +- .../ContentTypeServiceVariantsTests.cs | 2 +- .../TestHelpers/TestSyncBootStateAccessor.cs | 3 + ...aseServerRegistrarAndMessengerComponent.cs | 75 +-------- ...baseServerRegistrarAndMessengerComposer.cs | 38 +++++ .../NuCache/PublishedSnapshotService.cs | 24 +-- .../Search/ExamineFinalComponent.cs | 58 +++++-- src/Umbraco.Web/Umbraco.Web.csproj | 3 +- 15 files changed, 198 insertions(+), 189 deletions(-) create mode 100644 src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComposer.cs diff --git a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs index ebc77dbdca..2b52a0948e 100644 --- a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs +++ b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs @@ -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 _getSyncBootState; public DatabaseServerMessengerOptions Options { get; } @@ -59,6 +59,7 @@ namespace Umbraco.Core.Sync _lastPruned = _lastSync = DateTime.UtcNow; _syncIdle = new ManualResetEvent(true); _distCacheFilePath = new Lazy(() => GetDistCacheFilePath(globalSettings)); + _getSyncBootState = new Lazy(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( @@ -118,6 +119,12 @@ namespace Umbraco.Core.Sync /// Callers MUST ensure thread-safety. /// protected void Boot() + { + var bootState = GetSyncBootState(); + Booting?.Invoke(this, bootState); + } + + 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 +146,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("The wait lock timed out, application is shutting down. The current instruction batch will be re-processed."); @@ -147,17 +154,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; } /// @@ -167,36 +180,11 @@ 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. /// - private void Initialize(IUmbracoDatabase database) + private SyncBootState Initialize(IUmbracoDatabase database) { - lock (_locko) - { - if (_released) return; - var coldboot = IsColdBoot(database); + // could occur if shutting down immediately once starting up and before we've initialized + if (_released) return SyncBootState.Unknown; - 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("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; - } - } - - private bool IsColdBoot(IUmbracoDatabase database) - { var coldboot = false; if (_lastId < 0) // never synced before { @@ -206,27 +194,48 @@ namespace Umbraco.Core.Sync + " 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 + 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("SELECT SUM(instructionCount) FROM umbracoCacheInstruction WHERE id > @lastId", new { lastId = _lastId }); + if (count > Options.MaxProcessingInstructionCount) { - //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("SELECT SUM(instructionCount) FROM umbracoCacheInstruction WHERE id > @lastId", new {lastId = _lastId}); - if (count > Options.MaxProcessingInstructionCount) - { - //too many instructions, proceed to cold boot - Logger.Warn( - "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); + //too many instructions, proceed to cold boot + Logger.Warn( + "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; } } - return coldboot; + 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("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(); + } + } + } + + return coldboot ? SyncBootState.ColdBoot : SyncBootState.WarmBoot; } /// @@ -358,7 +367,7 @@ namespace Umbraco.Core.Sync } catch (JsonException ex) { - Logger.Error(ex, "Failed to deserialize instructions ({DtoId}: '{DtoInstructions}').", + Logger.Error(ex, "Failed to deserialize instructions ({DtoId}: '{DtoInstructions}').", dto.Id, dto.Instructions); @@ -416,11 +425,11 @@ namespace Umbraco.Core.Sync //} catch (Exception ex) { - Logger.Error ( - ex, - "DISTRIBUTED CACHE IS NOT UPDATED. Failed to execute instructions ({DtoId}: '{DtoInstructions}'). Instruction is being skipped/ignored", - dto.Id, - dto.Instructions); + Logger.Error( + 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. @@ -536,6 +545,8 @@ namespace Umbraco.Core.Sync + "/D" + AppDomain.CurrentDomain.Id // eg 22 + "] " + Guid.NewGuid().ToString("N").ToUpper(); // make it truly unique + public event EventHandler Booting; + private string GetDistCacheFilePath(IGlobalSettings globalSettings) { var fileName = HttpRuntime.AppDomainAppId.ReplaceNonAlphanumericChars(string.Empty) + "-lastsynced.txt"; @@ -554,29 +565,7 @@ namespace Umbraco.Core.Sync #endregion - public SyncBootState GetSyncBootState() - { - try - { - ReadLastSynced(); // get _lastId - using (var scope = ScopeProvider.CreateScope()) - { - EnsureInstructions(scope.Database); - bool isColdBoot = IsColdBoot(scope.Database); - - if (isColdBoot) - { - return SyncBootState.ColdBoot; - } - return SyncBootState.HasSyncState; - } - } - catch(Exception ex) - { - Logger.Warn("Error determining Sync Boot State", ex); - return SyncBootState.Unknown; - } - } + public SyncBootState GetSyncBootState() => _getSyncBootState.Value; #region Notify refreshers diff --git a/src/Umbraco.Core/Sync/DatabaseServerMessengerOptions.cs b/src/Umbraco.Core/Sync/DatabaseServerMessengerOptions.cs index 6bfd6bff4a..a769709d09 100644 --- a/src/Umbraco.Core/Sync/DatabaseServerMessengerOptions.cs +++ b/src/Umbraco.Core/Sync/DatabaseServerMessengerOptions.cs @@ -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 /// public int MaxProcessingInstructionCount { get; set; } - /// - /// A list of callbacks that will be invoked if the lastsynced.txt file does not exist. - /// - /// - /// 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. - /// + [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 InitializingCallbacks { get; set; } /// diff --git a/src/Umbraco.Core/Sync/ISyncBootStateAccessor.cs b/src/Umbraco.Core/Sync/ISyncBootStateAccessor.cs index 4b8500f2d9..1598215d2c 100644 --- a/src/Umbraco.Core/Sync/ISyncBootStateAccessor.cs +++ b/src/Umbraco.Core/Sync/ISyncBootStateAccessor.cs @@ -1,8 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Umbraco.Core.Sync { @@ -16,5 +12,10 @@ namespace Umbraco.Core.Sync /// /// SyncBootState GetSyncBootState(); + + /// + /// Raised when the boot state is known + /// + event EventHandler Booting; } } diff --git a/src/Umbraco.Core/Sync/NonRuntimeLevelBootStateAccessor.cs b/src/Umbraco.Core/Sync/NonRuntimeLevelBootStateAccessor.cs index 70cec6cc96..cc0b7dfaf1 100644 --- a/src/Umbraco.Core/Sync/NonRuntimeLevelBootStateAccessor.cs +++ b/src/Umbraco.Core/Sync/NonRuntimeLevelBootStateAccessor.cs @@ -11,6 +11,8 @@ namespace Umbraco.Core.Sync /// public class NonRuntimeLevelBootStateAccessor : ISyncBootStateAccessor { + public event EventHandler Booting; + public SyncBootState GetSyncBootState() { return SyncBootState.Unknown; diff --git a/src/Umbraco.Core/Sync/SyncBootState.cs b/src/Umbraco.Core/Sync/SyncBootState.cs index 4abc53abba..45f7a87331 100644 --- a/src/Umbraco.Core/Sync/SyncBootState.cs +++ b/src/Umbraco.Core/Sync/SyncBootState.cs @@ -1,24 +1,20 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Umbraco.Core.Sync +namespace Umbraco.Core.Sync { public enum SyncBootState { /// - /// Unknown state. Treat as HasSyncState + /// Unknown state. Treat as WarmBoot /// Unknown = 0, + /// /// Cold boot. No Sync state /// ColdBoot = 1, + /// /// Warm boot. Sync state present /// - HasSyncState = 2 + WarmBoot = 2 } } diff --git a/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs b/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs index 068a161268..36f6b9d479 100644 --- a/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs +++ b/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs @@ -157,7 +157,7 @@ namespace Umbraco.Tests.PublishedContent Mock.Of(), Mock.Of(), new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() }), - new TestSyncBootStateAccessor(SyncBootState.HasSyncState)); + new TestSyncBootStateAccessor(SyncBootState.WarmBoot)); // invariant is the current default _variationAccesor.VariationContext = new VariationContext(); diff --git a/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs b/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs index 652891c476..7cb4d97ea7 100644 --- a/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs +++ b/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs @@ -203,7 +203,7 @@ namespace Umbraco.Tests.PublishedContent Mock.Of(), Mock.Of(), new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() }), - new TestSyncBootStateAccessor(SyncBootState.HasSyncState)); + new TestSyncBootStateAccessor(SyncBootState.WarmBoot)); // invariant is the current default _variationAccesor.VariationContext = new VariationContext(); diff --git a/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs b/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs index e204bd7b5f..4453bfbbdf 100644 --- a/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs +++ b/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs @@ -100,7 +100,7 @@ namespace Umbraco.Tests.Scoping Factory.GetInstance(), Mock.Of(), new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() }), - new TestSyncBootStateAccessor(SyncBootState.HasSyncState)); + new TestSyncBootStateAccessor(SyncBootState.WarmBoot)); } protected UmbracoContext GetUmbracoContextNu(string url, int templateId = 1234, RouteData routeData = null, bool setSingleton = false, IUmbracoSettingsSection umbracoSettings = null, IEnumerable urlProviders = null) diff --git a/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs index 9cd4bb63e8..7e3bc60411 100644 --- a/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs +++ b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs @@ -72,7 +72,7 @@ namespace Umbraco.Tests.Services Factory.GetInstance(), Mock.Of(), new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() }), - new TestSyncBootStateAccessor(SyncBootState.HasSyncState)); + new TestSyncBootStateAccessor(SyncBootState.WarmBoot)); } public class LocalServerMessenger : ServerMessengerBase diff --git a/src/Umbraco.Tests/TestHelpers/TestSyncBootStateAccessor.cs b/src/Umbraco.Tests/TestHelpers/TestSyncBootStateAccessor.cs index e5f6989381..3bff19ff2d 100644 --- a/src/Umbraco.Tests/TestHelpers/TestSyncBootStateAccessor.cs +++ b/src/Umbraco.Tests/TestHelpers/TestSyncBootStateAccessor.cs @@ -15,6 +15,9 @@ namespace Umbraco.Tests.TestHelpers { _syncBootState = syncBootState; } + + public event EventHandler Booting; + public SyncBootState GetSyncBootState() { return _syncBootState; diff --git a/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs b/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs index 26ba0db324..ef13e166a5 100644 --- a/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs +++ b/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs @@ -4,77 +4,13 @@ using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Logging; using Umbraco.Core.Services; -using Umbraco.Core.Services.Changes; using Umbraco.Core.Sync; using Umbraco.Examine; -using Umbraco.Web.Cache; using Umbraco.Web.Routing; using Umbraco.Web.Scheduling; -using Umbraco.Web.Search; -using Current = Umbraco.Web.Composing.Current; namespace Umbraco.Web.Compose { - /// - /// Ensures that servers are automatically registered in the database, when using the database server registrar. - /// - /// - /// At the moment servers are automatically registered upon first request and then on every - /// request but not more than once per (configurable) period. This really is "for information & debug" purposes so - /// we can look at the table and see what servers are registered - but the info is not used anywhere. - /// Should we actually want to use this, we would need a better and more deterministic way of figuring - /// out the "server address" ie the address to which server-to-server requests should be sent - because it - /// probably is not the "current request address" - especially in multi-domains configurations. - /// - [RuntimeLevel(MinLevel = RuntimeLevel.Run)] - - // during Initialize / Startup, we end up checking Examine, which needs to be initialized beforehand - // TODO: should not be a strong dependency on "examine" but on an "indexing component" - [ComposeAfter(typeof(ExamineComposer))] - - public sealed class DatabaseServerRegistrarAndMessengerComposer : ComponentComposer, ICoreComposer - { - public static DatabaseServerMessengerOptions GetDefaultOptions(IFactory factory) - { - var logger = factory.GetInstance(); - var indexRebuilder = factory.GetInstance(); - - return new DatabaseServerMessengerOptions - { - //These callbacks will be executed if the server has not been synced - // (i.e. it is a new server or the lastsynced.txt file has been removed) - InitializingCallbacks = new Action[] - { - //rebuild the xml cache file if the server is not synced - () => - { - // rebuild the published snapshot caches entirely, if the server is not synced - // this is equivalent to DistributedCache RefreshAll... but local only - // (we really should have a way to reuse RefreshAll... locally) - // note: refresh all content & media caches does refresh content types too - var svc = Current.PublishedSnapshotService; - svc.Notify(new[] { new DomainCacheRefresher.JsonPayload(0, DomainChangeTypes.RefreshAll) }); - svc.Notify(new[] { new ContentCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }, out _, out _); - svc.Notify(new[] { new MediaCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }, out _); - }, - - //rebuild indexes if the server is not synced - // NOTE: This will rebuild ALL indexes including the members, if developers want to target specific - // indexes then they can adjust this logic themselves. - () => { ExamineComponent.RebuildIndexes(indexRebuilder, logger, false, 5000); } - } - }; - } - - public override void Compose(Composition composition) - { - base.Compose(composition); - - composition.SetDatabaseServerMessengerOptions(GetDefaultOptions); - composition.SetServerMessenger(); - composition.Register(factory=> factory.GetInstance() as BatchedDatabaseServerMessenger, Lifetime.Singleton); - } - } public sealed class DatabaseServerRegistrarAndMessengerComponent : IComponent { @@ -88,14 +24,17 @@ namespace Umbraco.Web.Compose private readonly BackgroundTaskRunner _processTaskRunner; private bool _started; private IBackgroundTask[] _tasks; - private IndexRebuilder _indexRebuilder; - public DatabaseServerRegistrarAndMessengerComponent(IRuntimeState runtime, IServerRegistrar serverRegistrar, IServerMessenger serverMessenger, IServerRegistrationService registrationService, ILogger logger, IndexRebuilder indexRebuilder) + public DatabaseServerRegistrarAndMessengerComponent( + IRuntimeState runtime, + IServerRegistrar serverRegistrar, + IServerMessenger serverMessenger, + IServerRegistrationService registrationService, + ILogger logger) { _runtime = runtime; _logger = logger; _registrationService = registrationService; - _indexRebuilder = indexRebuilder; // create task runner for DatabaseServerRegistrar _registrar = serverRegistrar as DatabaseServerRegistrar; @@ -118,7 +57,9 @@ namespace Umbraco.Web.Compose { //We will start the whole process when a successful request is made if (_registrar != null || _messenger != null) + { UmbracoModule.RouteAttempt += RegisterBackgroundTasksOnce; + } // must come last, as it references some _variables _messenger?.Startup(); diff --git a/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComposer.cs b/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComposer.cs new file mode 100644 index 0000000000..96e538652f --- /dev/null +++ b/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComposer.cs @@ -0,0 +1,38 @@ +using Umbraco.Core; +using Umbraco.Core.Composing; +using Umbraco.Core.Sync; +using Umbraco.Web.Search; + +namespace Umbraco.Web.Compose +{ + /// + /// Ensures that servers are automatically registered in the database, when using the database server registrar. + /// + /// + /// At the moment servers are automatically registered upon first request and then on every + /// request but not more than once per (configurable) period. This really is "for information & debug" purposes so + /// we can look at the table and see what servers are registered - but the info is not used anywhere. + /// Should we actually want to use this, we would need a better and more deterministic way of figuring + /// out the "server address" ie the address to which server-to-server requests should be sent - because it + /// probably is not the "current request address" - especially in multi-domains configurations. + /// + [RuntimeLevel(MinLevel = RuntimeLevel.Run)] + // TODO: This is legacy, we no longer need to do this but we don't want to change the behavior now + [ComposeAfter(typeof(ExamineComposer))] + public sealed class DatabaseServerRegistrarAndMessengerComposer : ComponentComposer, ICoreComposer + { + public static DatabaseServerMessengerOptions GetDefaultOptions(IFactory factory) + { + return new DatabaseServerMessengerOptions(); + } + + public override void Compose(Composition composition) + { + base.Compose(composition); + + composition.SetDatabaseServerMessengerOptions(GetDefaultOptions); + composition.SetServerMessenger(); + composition.Register(factory => factory.GetInstance() as BatchedDatabaseServerMessenger, Lifetime.Singleton); + } + } +} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index f3298cda54..b35a3d0edb 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -152,7 +152,10 @@ namespace Umbraco.Web.PublishedCache.NuCache _domainStore = new SnapDictionary(); - LoadCachesOnStartup(); + _syncBootStateAccessor.Booting += (sender, args) => + { + LoadCachesOnStartup(args); + }; } Guid GetUid(ContentStore store, int id) => store.LiveSnapshot.Get(id)?.Uid ?? default; @@ -219,32 +222,31 @@ namespace Umbraco.Web.PublishedCache.NuCache /// Populates the stores /// /// This is called inside of a lock for _storesLock - private void LoadCachesOnStartup() + private void LoadCachesOnStartup(SyncBootState bootState) { + // TODO: This is super ugly that this does this as part of the ctor. + // In netcore this will be different, the initialization will happen + // outside of the ctor. + var okContent = false; var okMedia = false; - if (_syncBootStateAccessor.GetSyncBootState() == SyncBootState.ColdBoot) - { - _logger.Info("Sync Service is in a Cold Boot state. Skip LoadCachesOnStartup as the Sync Service will trigger a full reload"); - _isReady = true; - return; - } + try { - if (_localContentDbExists) + if (bootState != SyncBootState.ColdBoot && _localContentDbExists) { okContent = LockAndLoadContent(scope => LoadContentFromLocalDbLocked(true)); if (!okContent) _logger.Warn("Loading content from local db raised warnings, will reload from database."); } - if (_localMediaDbExists) + if (bootState != SyncBootState.ColdBoot && _localMediaDbExists) { okMedia = LockAndLoadMedia(scope => LoadMediaFromLocalDbLocked(true)); if (!okMedia) _logger.Warn("Loading media from local db raised warnings, will reload from database."); } - + if (!okContent) LockAndLoadContent(scope => LoadContentFromDatabaseLocked(scope, true)); diff --git a/src/Umbraco.Web/Search/ExamineFinalComponent.cs b/src/Umbraco.Web/Search/ExamineFinalComponent.cs index 95000b2b46..22dcb43cc0 100644 --- a/src/Umbraco.Web/Search/ExamineFinalComponent.cs +++ b/src/Umbraco.Web/Search/ExamineFinalComponent.cs @@ -3,6 +3,8 @@ using Umbraco.Core.Logging; using Umbraco.Examine; using Umbraco.Core.Composing; using Umbraco.Core; +using Umbraco.Core.Sync; +using Umbraco.Web.Routing; namespace Umbraco.Web.Search { @@ -16,27 +18,65 @@ namespace Umbraco.Web.Search private readonly IExamineManager _examineManager; BackgroundIndexRebuilder _indexRebuilder; private readonly IMainDom _mainDom; - - public ExamineFinalComponent(IProfilingLogger logger, IExamineManager examineManager, BackgroundIndexRebuilder indexRebuilder, IMainDom mainDom) + private readonly ISyncBootStateAccessor _syncBootStateAccessor; + private readonly object _locker = new object(); + private bool _initialized = false; + + public ExamineFinalComponent(IProfilingLogger logger, IExamineManager examineManager, BackgroundIndexRebuilder indexRebuilder, IMainDom mainDom, ISyncBootStateAccessor syncBootStateAccessor) { _logger = logger; _examineManager = examineManager; _indexRebuilder = indexRebuilder; _mainDom = mainDom; + _syncBootStateAccessor = syncBootStateAccessor; + + // must add the handler in the ctor because it will be too late in Initialize + // TODO: All of this boot synchronization for cold boot logic needs should be fixed in netcore + _syncBootStateAccessor.Booting += _syncBootStateAccessor_Booting; + } + + /// + /// Once the boot state is known we can see if we require rebuilds + /// + /// + /// + private void _syncBootStateAccessor_Booting(object sender, SyncBootState e) + { + UmbracoModule.RouteAttempt += UmbracoModule_RouteAttempt; + } + + private void UmbracoModule_RouteAttempt(object sender, RoutableAttemptEventArgs e) + { + UmbracoModule.RouteAttempt -= UmbracoModule_RouteAttempt; + + if (!_initialized) + { + lock (_locker) + { + // double check lock, we must only do this once + if (!_initialized) + { + if (!_mainDom.IsMainDom) return; + + var bootState = _syncBootStateAccessor.GetSyncBootState(); + + _examineManager.ConfigureIndexes(_mainDom, _logger); + + // if it's a cold boot, rebuild all indexes including non-empty ones + _indexRebuilder.RebuildIndexes(bootState != SyncBootState.ColdBoot, 0); + } + } + } + } public void Initialize() - { - if (!_mainDom.IsMainDom) return; - - _examineManager.ConfigureIndexes(_mainDom, _logger); - - // TODO: Instead of waiting 5000 ms, we could add an event handler on to fulfilling the first request, then start? - _indexRebuilder.RebuildIndexes(true, 5000); + { } public void Terminate() { + _syncBootStateAccessor.Booting -= _syncBootStateAccessor_Booting; } } } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index a6cbefa825..48cc00afc9 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -132,6 +132,7 @@ + @@ -1306,4 +1307,4 @@ - + \ No newline at end of file From 1a3165507ef32aca8a9ec22501ff1bca6dcc2df2 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 26 Apr 2021 18:26:49 +1000 Subject: [PATCH 5/8] adds note --- src/Umbraco.Core/Sync/ISyncBootStateAccessor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Sync/ISyncBootStateAccessor.cs b/src/Umbraco.Core/Sync/ISyncBootStateAccessor.cs index 1598215d2c..249b038cae 100644 --- a/src/Umbraco.Core/Sync/ISyncBootStateAccessor.cs +++ b/src/Umbraco.Core/Sync/ISyncBootStateAccessor.cs @@ -16,6 +16,6 @@ namespace Umbraco.Core.Sync /// /// Raised when the boot state is known /// - event EventHandler Booting; + event EventHandler Booting; // TODO: This should be removed in netcore } } From 43ee6f288ea0b4249a6f411b03f29273fa809837 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 24 May 2021 11:25:03 -0700 Subject: [PATCH 6/8] fixes initialize flag --- src/Umbraco.Web/Search/ExamineFinalComponent.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Search/ExamineFinalComponent.cs b/src/Umbraco.Web/Search/ExamineFinalComponent.cs index 22dcb43cc0..335d19baf0 100644 --- a/src/Umbraco.Web/Search/ExamineFinalComponent.cs +++ b/src/Umbraco.Web/Search/ExamineFinalComponent.cs @@ -56,6 +56,8 @@ namespace Umbraco.Web.Search // double check lock, we must only do this once if (!_initialized) { + _initialized = true; + if (!_mainDom.IsMainDom) return; var bootState = _syncBootStateAccessor.GetSyncBootState(); @@ -63,7 +65,8 @@ namespace Umbraco.Web.Search _examineManager.ConfigureIndexes(_mainDom, _logger); // if it's a cold boot, rebuild all indexes including non-empty ones - _indexRebuilder.RebuildIndexes(bootState != SyncBootState.ColdBoot, 0); + // delay one minute since a cold boot also triggers nucache rebuilds + _indexRebuilder.RebuildIndexes(bootState != SyncBootState.ColdBoot, 60000); } } } From be3bfe1b6370d5d2cfded7a569c9fa1a969baa20 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 24 May 2021 12:32:57 -0700 Subject: [PATCH 7/8] Removes Booting event, backports lazy changes for PublishedSnapshotService from v9. --- .../Sync/ISyncBootStateAccessor.cs | 5 - .../NuCache/PublishedSnapshotService.cs | 202 ++++++++++-------- .../Search/ExamineFinalComponent.cs | 22 +- 3 files changed, 119 insertions(+), 110 deletions(-) diff --git a/src/Umbraco.Core/Sync/ISyncBootStateAccessor.cs b/src/Umbraco.Core/Sync/ISyncBootStateAccessor.cs index 249b038cae..8d4cda093c 100644 --- a/src/Umbraco.Core/Sync/ISyncBootStateAccessor.cs +++ b/src/Umbraco.Core/Sync/ISyncBootStateAccessor.cs @@ -12,10 +12,5 @@ namespace Umbraco.Core.Sync /// /// SyncBootState GetSyncBootState(); - - /// - /// Raised when the boot state is known - /// - event EventHandler Booting; // TODO: This should be removed in netcore } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index b35a3d0edb..f70160cde8 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -36,6 +36,8 @@ namespace Umbraco.Web.PublishedCache.NuCache internal class PublishedSnapshotService : PublishedSnapshotServiceBase { + private readonly PublishedSnapshotServiceOptions _options; + private readonly IMainDom _mainDom; private readonly ServiceContext _serviceContext; private readonly IPublishedContentTypeFactory _publishedContentTypeFactory; private readonly IScopeProvider _scopeProvider; @@ -50,12 +52,13 @@ namespace Umbraco.Web.PublishedCache.NuCache private readonly IDefaultCultureAccessor _defaultCultureAccessor; private readonly UrlSegmentProviderCollection _urlSegmentProviders; - // volatile because we read it with no lock - private volatile bool _isReady; + private bool _isReady; + private bool _isReadSet; + private object _isReadyLock; - private readonly ContentStore _contentStore; - private readonly ContentStore _mediaStore; - private readonly SnapDictionary _domainStore; + private ContentStore _contentStore; + private ContentStore _mediaStore; + private SnapDictionary _domainStore; private readonly object _storesLock = new object(); private readonly object _elementsLock = new object(); @@ -88,9 +91,12 @@ namespace Umbraco.Web.PublishedCache.NuCache ISyncBootStateAccessor syncBootStateAccessor) : base(publishedSnapshotAccessor, variationContextAccessor) { + //if (Interlocked.Increment(ref _singletonCheck) > 1) // throw new Exception("Singleton must be instantiated only once!"); + _options = options; + _mainDom = mainDom; _serviceContext = serviceContext; _publishedContentTypeFactory = publishedContentTypeFactory; _dataSource = dataSource; @@ -123,44 +129,6 @@ namespace Umbraco.Web.PublishedCache.NuCache if (runtime.Level != RuntimeLevel.Run) return; - // lock this entire call, we only want a single thread to be accessing the stores at once and within - // the call below to mainDom.Register, a callback may occur on a threadpool thread to MainDomRelease - // at the same time as we are trying to write to the stores. MainDomRelease also locks on _storesLock so - // it will not be able to close the stores until we are done populating (if the store is empty) - lock (_storesLock) - { - if (options.IgnoreLocalDb == false) - { - var registered = mainDom.Register(MainDomRegister, MainDomRelease); - - // stores are created with a db so they can write to it, but they do not read from it, - // stores need to be populated, happens in OnResolutionFrozen which uses _localDbExists to - // figure out whether it can read the databases or it should populate them from sql - - _logger.Info("Creating the content store, localContentDbExists? {LocalContentDbExists}", _localContentDbExists); - _contentStore = new ContentStore(publishedSnapshotAccessor, variationContextAccessor, logger, _localContentDb); - _logger.Info("Creating the media store, localMediaDbExists? {LocalMediaDbExists}", _localMediaDbExists); - _mediaStore = new ContentStore(publishedSnapshotAccessor, variationContextAccessor, logger, _localMediaDb); - } - else - { - _logger.Info("Creating the content store (local db ignored)"); - _contentStore = new ContentStore(publishedSnapshotAccessor, variationContextAccessor, logger); - _logger.Info("Creating the media store (local db ignored)"); - _mediaStore = new ContentStore(publishedSnapshotAccessor, variationContextAccessor, logger); - } - - _domainStore = new SnapDictionary(); - - _syncBootStateAccessor.Booting += (sender, args) => - { - LoadCachesOnStartup(args); - }; - } - - Guid GetUid(ContentStore store, int id) => store.LiveSnapshot.Get(id)?.Uid ?? default; - int GetId(ContentStore store, Guid uid) => store.LiveSnapshot.Get(uid)?.Id ?? default; - if (idkMap != null) { idkMap.SetMapper(UmbracoObjectTypes.Document, id => GetUid(_contentStore, id), uid => GetId(_contentStore, uid)); @@ -168,6 +136,18 @@ namespace Umbraco.Web.PublishedCache.NuCache } } + private int GetId(ContentStore store, Guid uid) + { + EnsureCaches(); + return store.LiveSnapshot.Get(uid)?.Id ?? default; + } + + private Guid GetUid(ContentStore store, int id) + { + EnsureCaches(); + return store.LiveSnapshot.Get(id)?.Uid ?? default; + } + /// /// Install phase of /// @@ -219,51 +199,82 @@ namespace Umbraco.Web.PublishedCache.NuCache } /// - /// Populates the stores + /// Lazily populates the stores only when they are first requested /// - /// This is called inside of a lock for _storesLock - private void LoadCachesOnStartup(SyncBootState bootState) - { - // TODO: This is super ugly that this does this as part of the ctor. - // In netcore this will be different, the initialization will happen - // outside of the ctor. - - var okContent = false; - var okMedia = false; - - try + internal void EnsureCaches() => LazyInitializer.EnsureInitialized( + ref _isReady, + ref _isReadSet, + ref _isReadyLock, + () => { - if (bootState != SyncBootState.ColdBoot && _localContentDbExists) + // lock this entire call, we only want a single thread to be accessing the stores at once and within + // the call below to mainDom.Register, a callback may occur on a threadpool thread to MainDomRelease + // at the same time as we are trying to write to the stores. MainDomRelease also locks on _storesLock so + // it will not be able to close the stores until we are done populating (if the store is empty) + lock (_storesLock) { - okContent = LockAndLoadContent(scope => LoadContentFromLocalDbLocked(true)); - if (!okContent) - _logger.Warn("Loading content from local db raised warnings, will reload from database."); + if (!_options.IgnoreLocalDb) + { + var registered = _mainDom.Register(MainDomRegister, MainDomRelease); + + // stores are created with a db so they can write to it, but they do not read from it, + // stores need to be populated, happens in OnResolutionFrozen which uses _localDbExists to + // figure out whether it can read the databases or it should populate them from sql + + _logger.Info("Creating the content store, localContentDbExists? {LocalContentDbExists}", _localContentDbExists); + _contentStore = new ContentStore(PublishedSnapshotAccessor, VariationContextAccessor, _logger, _localContentDb); + _logger.Info("Creating the media store, localMediaDbExists? {LocalMediaDbExists}", _localMediaDbExists); + _mediaStore = new ContentStore(PublishedSnapshotAccessor, VariationContextAccessor, _logger, _localMediaDb); + } + else + { + _logger.Info("Creating the content store (local db ignored)"); + _contentStore = new ContentStore(PublishedSnapshotAccessor, VariationContextAccessor, _logger); + _logger.Info("Creating the media store (local db ignored)"); + _mediaStore = new ContentStore(PublishedSnapshotAccessor, VariationContextAccessor, _logger); + } + + _domainStore = new SnapDictionary(); + + SyncBootState bootState = _syncBootStateAccessor.GetSyncBootState(); + + var okContent = false; + var okMedia = false; + + try + { + if (bootState != SyncBootState.ColdBoot && _localContentDbExists) + { + okContent = LockAndLoadContent(scope => LoadContentFromLocalDbLocked(true)); + if (!okContent) + _logger.Warn("Loading content from local db raised warnings, will reload from database."); + } + + if (bootState != SyncBootState.ColdBoot && _localMediaDbExists) + { + okMedia = LockAndLoadMedia(scope => LoadMediaFromLocalDbLocked(true)); + if (!okMedia) + _logger.Warn("Loading media from local db raised warnings, will reload from database."); + } + + if (!okContent) + LockAndLoadContent(scope => LoadContentFromDatabaseLocked(scope, true)); + + if (!okMedia) + LockAndLoadMedia(scope => LoadMediaFromDatabaseLocked(scope, true)); + + LockAndLoadDomains(); + } + catch (Exception ex) + { + _logger.Fatal(ex, "Panic, exception while loading cache data."); + throw; + } + + // finally, cache is ready! + return true; } - - if (bootState != SyncBootState.ColdBoot && _localMediaDbExists) - { - okMedia = LockAndLoadMedia(scope => LoadMediaFromLocalDbLocked(true)); - if (!okMedia) - _logger.Warn("Loading media from local db raised warnings, will reload from database."); - } - - if (!okContent) - LockAndLoadContent(scope => LoadContentFromDatabaseLocked(scope, true)); - - if (!okMedia) - LockAndLoadMedia(scope => LoadMediaFromDatabaseLocked(scope, true)); - - LockAndLoadDomains(); - } - catch (Exception ex) - { - _logger.Fatal(ex, "Panic, exception while loading cache data."); - throw; - } - - // finally, cache is ready! - _isReady = true; - } + }); private void InitializeRepositoryEvents() { @@ -1146,9 +1157,13 @@ namespace Umbraco.Web.PublishedCache.NuCache public override IPublishedSnapshot CreatePublishedSnapshot(string previewToken) { + EnsureCaches(); + // no cache, no joy - if (_isReady == false) + if (Volatile.Read(ref _isReady) == false) + { throw new InvalidOperationException("The published snapshot service has not properly initialized."); + } var preview = previewToken.IsNullOrWhiteSpace() == false; return new PublishedSnapshot(this, preview); @@ -1159,6 +1174,8 @@ namespace Umbraco.Web.PublishedCache.NuCache // even though the underlying elements may not change (store snapshots) public PublishedSnapshot.PublishedSnapshotElements GetElements(bool previewDefault) { + EnsureCaches(); + // note: using ObjectCacheAppCache for elements and snapshot caches // is not recommended because it creates an inner MemoryCache which is a heavy // thing - better use a dictionary-based cache which "just" creates a concurrent @@ -1805,6 +1822,8 @@ AND cmsContentNu.nodeId IS NULL public void Collect() { + EnsureCaches(); + var contentCollect = _contentStore.CollectAsync(); var mediaCollect = _mediaStore.CollectAsync(); System.Threading.Tasks.Task.WaitAll(contentCollect, mediaCollect); @@ -1814,8 +1833,17 @@ AND cmsContentNu.nodeId IS NULL #region Internals/Testing - internal ContentStore GetContentStore() => _contentStore; - internal ContentStore GetMediaStore() => _mediaStore; + internal ContentStore GetContentStore() + { + EnsureCaches(); + return _contentStore; + } + + internal ContentStore GetMediaStore() + { + EnsureCaches(); + return _mediaStore; + } #endregion } diff --git a/src/Umbraco.Web/Search/ExamineFinalComponent.cs b/src/Umbraco.Web/Search/ExamineFinalComponent.cs index 335d19baf0..6dd775ab64 100644 --- a/src/Umbraco.Web/Search/ExamineFinalComponent.cs +++ b/src/Umbraco.Web/Search/ExamineFinalComponent.cs @@ -29,26 +29,10 @@ namespace Umbraco.Web.Search _indexRebuilder = indexRebuilder; _mainDom = mainDom; _syncBootStateAccessor = syncBootStateAccessor; - - // must add the handler in the ctor because it will be too late in Initialize - // TODO: All of this boot synchronization for cold boot logic needs should be fixed in netcore - _syncBootStateAccessor.Booting += _syncBootStateAccessor_Booting; - } - - /// - /// Once the boot state is known we can see if we require rebuilds - /// - /// - /// - private void _syncBootStateAccessor_Booting(object sender, SyncBootState e) - { - UmbracoModule.RouteAttempt += UmbracoModule_RouteAttempt; } private void UmbracoModule_RouteAttempt(object sender, RoutableAttemptEventArgs e) { - UmbracoModule.RouteAttempt -= UmbracoModule_RouteAttempt; - if (!_initialized) { lock (_locker) @@ -58,6 +42,8 @@ namespace Umbraco.Web.Search { _initialized = true; + UmbracoModule.RouteAttempt -= UmbracoModule_RouteAttempt; + if (!_mainDom.IsMainDom) return; var bootState = _syncBootStateAccessor.GetSyncBootState(); @@ -74,12 +60,12 @@ namespace Umbraco.Web.Search } public void Initialize() - { + { + UmbracoModule.RouteAttempt += UmbracoModule_RouteAttempt; } public void Terminate() { - _syncBootStateAccessor.Booting -= _syncBootStateAccessor_Booting; } } } From eba3c82a86ceea384a4a063d7b5bb50edf5ad6ed Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 24 May 2021 12:51:40 -0700 Subject: [PATCH 8/8] Changes startup logic, no more "Boot" method, the component just handles events --- .../Sync/DatabaseServerMessenger.cs | 16 +++------ .../BatchedDatabaseServerMessenger.cs | 33 ++++++++----------- ...aseServerRegistrarAndMessengerComponent.cs | 12 ++++--- 3 files changed, 26 insertions(+), 35 deletions(-) diff --git a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs index 2b52a0948e..75ccf5e4f9 100644 --- a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs +++ b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs @@ -111,17 +111,11 @@ namespace Umbraco.Core.Sync #region Sync - /// - /// Boots the messenger. - /// - /// - /// Thread safety: this is NOT thread safe. Because it is NOT meant to run multi-threaded. - /// Callers MUST ensure thread-safety. - /// + [Obsolete("This is no longer used and will be removed in future versions")] protected void Boot() { - var bootState = GetSyncBootState(); - Booting?.Invoke(this, bootState); + // if called, just forces the boot logic + _ = GetSyncBootState(); } private SyncBootState BootInternal() @@ -545,8 +539,6 @@ namespace Umbraco.Core.Sync + "/D" + AppDomain.CurrentDomain.Id // eg 22 + "] " + Guid.NewGuid().ToString("N").ToUpper(); // make it truly unique - public event EventHandler Booting; - private string GetDistCacheFilePath(IGlobalSettings globalSettings) { var fileName = HttpRuntime.AppDomainAppId.ReplaceNonAlphanumericChars(string.Empty) + "-lastsynced.txt"; @@ -565,7 +557,7 @@ namespace Umbraco.Core.Sync #endregion - public SyncBootState GetSyncBootState() => _getSyncBootState.Value; + public virtual SyncBootState GetSyncBootState() => _getSyncBootState.Value; #region Notify refreshers diff --git a/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs b/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs index 6596fee245..f89f62eb3d 100644 --- a/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs +++ b/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs @@ -26,6 +26,7 @@ namespace Umbraco.Web public class BatchedDatabaseServerMessenger : DatabaseServerMessenger { private readonly IUmbracoDatabaseFactory _databaseFactory; + private readonly Lazy _syncBootState; [Obsolete("This overload should not be used, enableDistCalls has no effect")] [EditorBrowsable(EditorBrowsableState.Never)] @@ -39,28 +40,22 @@ namespace Umbraco.Web : base(runtime, scopeProvider, sqlContext, proflog, globalSettings, true, options) { _databaseFactory = databaseFactory; - } - - // invoked by DatabaseServerRegistrarAndMessengerComponent - internal void Startup() - { - UmbracoModule.EndRequest += UmbracoModule_EndRequest; - - if (_databaseFactory.CanConnect == false) + _syncBootState = new Lazy(() => { - Logger.Warn("Cannot connect to the database, distributed calls will not be enabled for this server."); - } - else - { - Boot(); - } + if (_databaseFactory.CanConnect == false) + { + Logger.Warn("Cannot connect to the database, distributed calls will not be enabled for this server."); + return SyncBootState.Unknown; + } + else + { + return base.GetSyncBootState(); + } + }); } - private void UmbracoModule_EndRequest(object sender, UmbracoRequestEventArgs e) - { - // will clear the batch - will remain in HttpContext though - that's ok - FlushBatch(); - } + // override to deal with database connectivity + public override SyncBootState GetSyncBootState() => _syncBootState.Value; protected override void DeliverRemote(ICacheRefresher refresher, MessageType messageType, IEnumerable ids = null, string json = null) { diff --git a/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs b/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs index ef13e166a5..d696ae5527 100644 --- a/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs +++ b/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs @@ -59,15 +59,19 @@ namespace Umbraco.Web.Compose if (_registrar != null || _messenger != null) { UmbracoModule.RouteAttempt += RegisterBackgroundTasksOnce; - } - - // must come last, as it references some _variables - _messenger?.Startup(); + UmbracoModule.EndRequest += UmbracoModule_EndRequest; + } } public void Terminate() { } + private void UmbracoModule_EndRequest(object sender, UmbracoRequestEventArgs e) + { + // will clear the batch - will remain in HttpContext though - that's ok + _messenger?.FlushBatch(); + } + /// /// Handle when a request is made ///