diff --git a/src/Umbraco.Core/Configuration/Models/NuCacheSettings.cs b/src/Umbraco.Core/Configuration/Models/NuCacheSettings.cs index 7767d1dbdc..ee41fc32d3 100644 --- a/src/Umbraco.Core/Configuration/Models/NuCacheSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/NuCacheSettings.cs @@ -13,6 +13,7 @@ namespace Umbraco.Cms.Core.Configuration.Models { internal const string StaticNuCacheSerializerType = "MessagePack"; internal const int StaticSqlPageSize = 1000; + internal const int StaticKitBatchSize = 1; /// /// Gets or sets a value defining the BTree block size. @@ -31,6 +32,12 @@ namespace Umbraco.Cms.Core.Configuration.Models [DefaultValue(StaticSqlPageSize)] public int SqlPageSize { get; set; } = StaticSqlPageSize; + /// + /// The size to use for nucache Kit batches. Higher value means more content loaded into memory at a time. + /// + [DefaultValue(StaticKitBatchSize)] + public int KitBatchSize { get; set; } = StaticKitBatchSize; + public bool UnPublishedContentCompression { get; set; } = false; } } diff --git a/src/Umbraco.PublishedCache.NuCache/ContentStore.cs b/src/Umbraco.PublishedCache.NuCache/ContentStore.cs index ddb8ea9057..1ccc75331b 100644 --- a/src/Umbraco.PublishedCache.NuCache/ContentStore.cs +++ b/src/Umbraco.PublishedCache.NuCache/ContentStore.cs @@ -11,6 +11,7 @@ using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.PublishedCache.Snap; +using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.PublishedCache { @@ -342,7 +343,7 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache if (node == null) continue; var contentTypeId = node.ContentType.Id; if (index.TryGetValue(contentTypeId, out var contentType) == false) continue; - SetValueLocked(_contentNodes, node.Id, new ContentNode(node, _publishedModelFactory, contentType)); + SetValueLocked(_contentNodes, node.Id, new ContentNode(node, _publishedModelFactory, contentType)); } } @@ -515,7 +516,7 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache _contentNodes.TryGetValue(id, out var link); if (link?.Value == null) continue; - var node = new ContentNode(link.Value, _publishedModelFactory, contentType); + var node = new ContentNode(link.Value, _publishedModelFactory, contentType); SetValueLocked(_contentNodes, id, node); if (_localDb != null) RegisterChange(id, node.ToKit()); } @@ -631,12 +632,12 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache // moving? var moving = existing != null && existing.ParentContentId != kit.Node.ParentContentId; - // manage children - if (existing != null) - { - kit.Node.FirstChildContentId = existing.FirstChildContentId; - kit.Node.LastChildContentId = existing.LastChildContentId; - } + // manage children + if (existing != null) + { + kit.Node.FirstChildContentId = existing.FirstChildContentId; + kit.Node.LastChildContentId = existing.LastChildContentId; + } // set SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); @@ -696,7 +697,36 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache /// /// Thrown if this method is not called within a write lock /// + [Obsolete("Use the overload that takes a 'kitGroupSize' parameter instead")] public bool SetAllFastSortedLocked(IEnumerable kits, bool fromDb) + { + return SetAllFastSortedLocked(kits, 1, fromDb); + } + + /// + /// Builds all kits on startup using a fast forward only cursor + /// + /// + /// All kits sorted by Level + Parent Id + Sort order + /// + /// + /// True if the data is coming from the database (not the local cache db) + /// + /// + /// + /// This requires that the collection is sorted by Level + ParentId + Sort Order. + /// This should be used only on a site startup as the first generations. + /// This CANNOT be used after startup since it bypasses all checks for Generations. + /// + /// + /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock + /// otherwise an exception will occur. + /// + /// + /// + /// Thrown if this method is not called within a write lock + /// + public bool SetAllFastSortedLocked(IEnumerable kits, int kitGroupSize, bool fromDb) { EnsureLocked(); @@ -714,54 +744,67 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache ContentNode previousNode = null; ContentNode parent = null; - foreach (var kit in kits) + // By using InGroupsOf() here we are forcing the database query result extraction to retrieve items in batches, + // reducing the possibility of a database timeout (ThreadAbortException) on large datasets. + // This in turn reduces the possibility that the NuCache file will remain locked, because an exception + // here results in the calling method to not release the lock. + + // However the larger the batck size, the more content loaded into memory. So by default, this is set to 1 and can be increased by setting + // the configuration setting Umbraco:CMS:NuCache:KitPageSize to a higher value. + + // If we are not loading from the database, then we can ignore this restriction. + + foreach (var kitGroup in kits.InGroupsOf(!fromDb || kitGroupSize < 1 ? 1 : kitGroupSize)) { - if (!BuildKit(kit, out var parentLink)) + foreach (var kit in kitGroup) { - ok = false; - continue; // skip that one + if (!BuildKit(kit, out var parentLink)) + { + ok = false; + continue; // skip that one + } + + var thisNode = kit.Node; + + if (parent == null) + { + // first parent + parent = parentLink.Value; + parent.FirstChildContentId = thisNode.Id; // this node is the first node + } + else if (parent.Id != parentLink.Value.Id) + { + // new parent + parent = parentLink.Value; + parent.FirstChildContentId = thisNode.Id; // this node is the first node + previousNode = null; // there is no previous sibling + } + + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Set {thisNodeId} with parent {thisNodeParentContentId}", thisNode.Id, thisNode.ParentContentId); + } + + SetValueLocked(_contentNodes, thisNode.Id, thisNode); + + // if we are initializing from the database source ensure the local db is updated + if (fromDb && _localDb != null) RegisterChange(thisNode.Id, kit); + + // this node is always the last child + parent.LastChildContentId = thisNode.Id; + + // wire previous node as previous sibling + if (previousNode != null) + { + previousNode.NextSiblingContentId = thisNode.Id; + thisNode.PreviousSiblingContentId = previousNode.Id; + } + + // this node becomes the previous node + previousNode = thisNode; + + _contentKeyToIdMap[kit.Node.Uid] = kit.Node.Id; } - - var thisNode = kit.Node; - - if (parent == null) - { - // first parent - parent = parentLink.Value; - parent.FirstChildContentId = thisNode.Id; // this node is the first node - } - else if (parent.Id != parentLink.Value.Id) - { - // new parent - parent = parentLink.Value; - parent.FirstChildContentId = thisNode.Id; // this node is the first node - previousNode = null; // there is no previous sibling - } - - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Set {thisNodeId} with parent {thisNodeParentContentId}", thisNode.Id, thisNode.ParentContentId); - } - - SetValueLocked(_contentNodes, thisNode.Id, thisNode); - - // if we are initializing from the database source ensure the local db is updated - if (fromDb && _localDb != null) RegisterChange(thisNode.Id, kit); - - // this node is always the last child - parent.LastChildContentId = thisNode.Id; - - // wire previous node as previous sibling - if (previousNode != null) - { - previousNode.NextSiblingContentId = thisNode.Id; - thisNode.PreviousSiblingContentId = previousNode.Id; - } - - // this node becomes the previous node - previousNode = thisNode; - - _contentKeyToIdMap[kit.Node.Uid] = kit.Node.Id; } return ok; @@ -779,7 +822,27 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache /// /// Thrown if this method is not called within a write lock /// + [Obsolete("Use the overload that takes the 'kitGroupSize' and 'fromDb' parameters instead")] public bool SetAllLocked(IEnumerable kits) + { + return SetAllLocked(kits, 1, false); + } + + /// + /// Set all data for a collection of + /// + /// + /// + /// True if the data is coming from the database (not the local cache db) + /// + /// + /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock + /// otherwise an exception will occur. + /// + /// + /// Thrown if this method is not called within a write lock + /// + public bool SetAllLocked(IEnumerable kits, int kitGroupSize, bool fromDb) { EnsureLocked(); @@ -792,25 +855,37 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache //ClearLocked(_contentTypesById); //ClearLocked(_contentTypesByAlias); - foreach (var kit in kits) + // By using InGroupsOf() here we are forcing the database query result extraction to retrieve items in batches, + // reducing the possibility of a database timeout (ThreadAbortException) on large datasets. + // This in turn reduces the possibility that the NuCache file will remain locked, because an exception + // here results in the calling method to not release the lock. + + // However the larger the batck size, the more content loaded into memory. So by default, this is set to 1 and can be increased by setting + // the configuration setting Umbraco:CMS:NuCache:KitPageSize to a higher value. + + // If we are not loading from the database, then we can ignore this restriction. + foreach (var kitGroup in kits.InGroupsOf(!fromDb || kitGroupSize < 1 ? 1 : kitGroupSize)) { - if (!BuildKit(kit, out var parent)) + foreach (var kit in kitGroup) { - ok = false; - continue; // skip that one + if (!BuildKit(kit, out var parent)) + { + ok = false; + continue; // skip that one + } + + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Set {kitNodeId} with parent {kitNodeParentContentId}", kit.Node.Id, kit.Node.ParentContentId); + } + + SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); + + if (_localDb != null) RegisterChange(kit.Node.Id, kit); + AddTreeNodeLocked(kit.Node, parent); + + _contentKeyToIdMap[kit.Node.Uid] = kit.Node.Id; } - - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Set {kitNodeId} with parent {kitNodeParentContentId}", kit.Node.Id, kit.Node.ParentContentId); - } - - SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); - - if (_localDb != null) RegisterChange(kit.Node.Id, kit); - AddTreeNodeLocked(kit.Node, parent); - - _contentKeyToIdMap[kit.Node.Uid] = kit.Node.Id; } return ok; diff --git a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs index 20df726080..5dac6c1b9b 100644 --- a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs @@ -337,8 +337,19 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache _localContentDb?.Clear(); // IMPORTANT GetAllContentSources sorts kits by level + parentId + sortOrder - var kits = _publishedContentService.GetAllContentSources(); - return onStartup ? _contentStore.SetAllFastSortedLocked(kits, true) : _contentStore.SetAllLocked(kits); + + try + { + var kits = _publishedContentService.GetAllContentSources(); + return onStartup ? _contentStore.SetAllFastSortedLocked(kits, _config.KitBatchSize, true) : _contentStore.SetAllLocked(kits, _config.KitBatchSize, true); + } + catch (ThreadAbortException tae) + { + // Caught a ThreadAbortException, most likely from a database timeout. + // If we don't catch it here, the whole local cache can remain locked causing widespread panic (see above comment). + _logger.LogWarning(tae, tae.Message); + } + return false; } } @@ -385,8 +396,20 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache _logger.LogDebug("Loading media from database..."); // IMPORTANT GetAllMediaSources sorts kits by level + parentId + sortOrder - var kits = _publishedContentService.GetAllMediaSources(); - return onStartup ? _mediaStore.SetAllFastSortedLocked(kits, true) : _mediaStore.SetAllLocked(kits); + + try + { + + var kits = _publishedContentService.GetAllMediaSources(); + return onStartup ? _mediaStore.SetAllFastSortedLocked(kits, _config.KitBatchSize, true) : _mediaStore.SetAllLocked(kits, _config.KitBatchSize, true); + } + catch (ThreadAbortException tae) + { + // Caught a ThreadAbortException, most likely from a database timeout. + // If we don't catch it here, the whole local cache can remain locked causing widespread panic (see above comment). + _logger.LogWarning(tae, tae.Message); + } + return false; } } @@ -434,7 +457,7 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache return false; } - return onStartup ? store.SetAllFastSortedLocked(kits, false) : store.SetAllLocked(kits); + return onStartup ? store.SetAllFastSortedLocked(kits, _config.KitBatchSize, false) : store.SetAllLocked(kits, _config.KitBatchSize, false); } private void LockAndLoadDomains()