From af29b8da68d51a5d2a5f69c9d6ce04c3fd000ccf Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 4 May 2020 20:59:22 +1000 Subject: [PATCH 01/37] Cherry pick #8058 to fix #8038 --- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index 4433a8e307..27b016b2af 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -3,8 +3,10 @@ using System.Data; using System.Data.SqlClient; using System.Diagnostics; using System.Linq; +using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; +using System.Web; using Umbraco.Core.Logging; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; @@ -16,7 +18,7 @@ namespace Umbraco.Core.Runtime internal class SqlMainDomLock : IMainDomLock { private string _lockId; - private const string MainDomKey = "Umbraco.Core.Runtime.SqlMainDom"; + private const string MainDomKeyPrefix = "Umbraco.Core.Runtime.SqlMainDom"; private const string UpdatedSuffix = "_updated"; private readonly ILogger _logger; private IUmbracoDatabase _db; @@ -119,6 +121,16 @@ namespace Umbraco.Core.Runtime } + /// + /// Returns the keyvalue table key for the current server/app + /// + /// + /// The key is the the normal MainDomId which takes into account the AppDomainAppId and the physical file path of the app and this is + /// combined with the current machine name. The machine name is required because the default semaphore lock is machine wide so it implicitly + /// takes into account machine name whereas this needs to be explicitly per machine. + /// + private string MainDomKey { get; } = MainDomKeyPrefix + "-" + (NetworkHelper.MachineName + MainDom.GetMainDomId()).GenerateHash(); + private void ListeningLoop() { while (true) From 0b1e1e6dfe2950ed18714a9a783e45cce71a5d66 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 7 May 2020 15:48:38 +1000 Subject: [PATCH 02/37] Cherry pick: Adding section value check in the query string #8061 --- src/Umbraco.Web/Trees/ContentTreeControllerBase.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs index 6d156e3fc8..95de72b7bf 100644 --- a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs @@ -322,8 +322,10 @@ namespace Umbraco.Web.Trees var nodes = GetTreeNodesInternal(id, queryStrings); - //only render the recycle bin if we are not in dialog and the start id id still the root - if (IsDialog(queryStrings) == false && id == Constants.System.RootString) + //only render the recycle bin if we are not in dialog and the start id is still the root + //we need to check for the "application" key in the queryString because its value is required here, + //and for some reason when there are no dashboards, this parameter is missing + if (IsDialog(queryStrings) == false && id == Constants.System.RootString && queryStrings.HasKey("application")) { nodes.Add(CreateTreeNode( RecycleBinId.ToInvariantString(), From b2c2e869716876e6ab2552f2ff1dfc94fde3be03 Mon Sep 17 00:00:00 2001 From: Jeavon Leopold Date: Wed, 22 Apr 2020 11:33:27 +0100 Subject: [PATCH 03/37] Add GetMainDom method to UmbracoApplication so GetRuntime can be overridden (cherry picked from commit c3f59602c21d6a07f553066af7e8dd742114fd83) --- src/Umbraco.Web/UmbracoApplication.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web/UmbracoApplication.cs b/src/Umbraco.Web/UmbracoApplication.cs index f8ee238da7..f5667a5a85 100644 --- a/src/Umbraco.Web/UmbracoApplication.cs +++ b/src/Umbraco.Web/UmbracoApplication.cs @@ -2,6 +2,7 @@ using System.Threading; using System.Web; using Umbraco.Core; +using Umbraco.Core.Logging; using Umbraco.Core.Logging.Serilog; using Umbraco.Core.Runtime; using Umbraco.Web.Runtime; @@ -17,16 +18,24 @@ namespace Umbraco.Web { var logger = SerilogLogger.CreateWithDefaultConfiguration(); + var runtime = new WebRuntime(this, logger, GetMainDom(logger)); + + return runtime; + } + + /// + /// Returns a new MainDom + /// + protected IMainDom GetMainDom(ILogger logger) + { // Determine if we should use the sql main dom or the default var appSettingMainDomLock = ConfigurationManager.AppSettings[Constants.AppSettings.MainDomLock]; var mainDomLock = appSettingMainDomLock == "SqlMainDomLock" ? (IMainDomLock)new SqlMainDomLock(logger) : new MainDomSemaphoreLock(logger); - - var runtime = new WebRuntime(this, logger, new MainDom(logger, mainDomLock)); - return runtime; + return new MainDom(logger, mainDomLock); } /// From 9f2c1cd3e633936f990e09d779dfd76e47365c65 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 4 May 2020 17:13:01 +1000 Subject: [PATCH 04/37] when we rebuild indexes in the back office we are actually calling index.CreateIndex() 2 times before populating (cherry picked from commit d7469e6576fbe4bf4d23f9046399b3899c0ff58c) --- src/Umbraco.Web/Editors/ExamineManagementController.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Umbraco.Web/Editors/ExamineManagementController.cs b/src/Umbraco.Web/Editors/ExamineManagementController.cs index 0953b41cac..cf1dfd5d5d 100644 --- a/src/Umbraco.Web/Editors/ExamineManagementController.cs +++ b/src/Umbraco.Web/Editors/ExamineManagementController.cs @@ -141,9 +141,6 @@ namespace Umbraco.Web.Editors try { - //clear and replace - index.CreateIndex(); - var cacheKey = "temp_indexing_op_" + index.Name; //put temp val in cache which is used as a rudimentary way to know when the indexing is done AppCaches.RuntimeCache.Insert(cacheKey, () => "tempValue", TimeSpan.FromMinutes(5)); From 2b4968119ea3380f0aff174807fee1cf79d48693 Mon Sep 17 00:00:00 2001 From: Enkel Media Date: Wed, 27 May 2020 14:39:03 +0200 Subject: [PATCH 05/37] Fixes #8004 umbnodepreview setting contentType alias to null (#8109) (cherry picked from commit f2ad7bb6315fa28f55c6f663bb1ecbf9af4f3ff6) --- .../components/property/umbproperty.directive.js | 13 +++++++++---- .../components/umbnodepreview.directive.js | 13 +++++++++---- .../src/views/components/property/umb-property.html | 2 +- .../src/views/components/umb-node-preview.html | 2 +- 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js index 9c33b35e82..ad62bcd3db 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js @@ -16,10 +16,15 @@ angular.module("umbraco.directives") replace: true, templateUrl: 'views/components/property/umb-property.html', link: function (scope) { - userService.getCurrentUser().then(function (u) { - var isAdmin = u.userGroups.indexOf('admin') !== -1; - scope.propertyAlias = (Umbraco.Sys.ServerVariables.isDebuggingEnabled === true || isAdmin) ? scope.property.alias : null; - }); + + scope.controlLabelTitle = null; + if(Umbraco.Sys.ServerVariables.isDebuggingEnabled) { + userService.getCurrentUser().then(function (u) { + if(u.allowedSections.indexOf("settings") !== -1 ? true : false) { + scope.controlLabelTitle = scope.property.alias; + } + }); + } }, //Define a controller for this directive to expose APIs to other directives controller: function ($scope) { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnodepreview.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnodepreview.directive.js index 9f1f7a0d2e..fd52c4d7ea 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnodepreview.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnodepreview.directive.js @@ -102,10 +102,15 @@ if (!scope.editLabelKey) { scope.editLabelKey = "general_edit"; } - userService.getCurrentUser().then(function (u) { - var isAdmin = u.userGroups.indexOf('admin') !== -1; - scope.alias = (Umbraco.Sys.ServerVariables.isDebuggingEnabled === true || isAdmin) ? scope.alias : null; - }); + + scope.nodeNameTitle = null; + if(Umbraco.Sys.ServerVariables.isDebuggingEnabled) { + userService.getCurrentUser().then(function (u) { + if (u.allowedSections.indexOf("settings") !== -1 ? true : false) { + scope.nodeNameTitle = scope.alias; + } + }); + } } var directive = { diff --git a/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html b/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html index ca57679f51..658b7c73ab 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html @@ -11,7 +11,7 @@ {{inheritsFrom}} - public class IndexRebuilder { + private readonly IProfilingLogger _logger; private readonly IEnumerable _populators; public IExamineManager ExamineManager { get; } + [Obsolete("Use constructor with all dependencies")] public IndexRebuilder(IExamineManager examineManager, IEnumerable populators) + : this(Current.ProfilingLogger, examineManager, populators) + { + } + + public IndexRebuilder(IProfilingLogger logger, IExamineManager examineManager, IEnumerable populators) { _populators = populators; + _logger = logger; ExamineManager = examineManager; } @@ -53,7 +63,14 @@ namespace Umbraco.Examine // run each populator over the indexes foreach(var populator in _populators) { - populator.Populate(indexes); + try + { + populator.Populate(indexes); + } + catch (Exception e) + { + _logger.Error(e, "Index populating failed for populator {Populator}", populator.GetType()); + } } } From 6aa49242f2e2c4453a3dc2d6cd9032151efaef83 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 7 Jul 2020 17:53:33 +1000 Subject: [PATCH 23/37] Don't try to reuse db instances, thsi can result in potential zombie transactions --- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 176 +++++++++------------ 1 file changed, 74 insertions(+), 102 deletions(-) diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index 5f5d0d607f..8e2e688d66 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -56,14 +56,13 @@ namespace Umbraco.Core.Runtime _logger.Debug("Acquiring lock..."); - var db = GetDatabase(); - var tempId = Guid.NewGuid().ToString(); + using var db = _dbFactory.CreateDatabase(); + using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted); + try { - db.BeginTransaction(IsolationLevel.ReadCommitted); - try { // wait to get a write lock @@ -73,6 +72,8 @@ namespace Umbraco.Core.Runtime { if (IsLockTimeoutException(ex)) { + // TODO: Do we want to retry? We haven't seen any timeout exceptions here so not sure it's important at this stage + _logger.Error(ex, "Sql timeout occurred, could not acquire MainDom."); _hasError = true; return false; @@ -82,15 +83,12 @@ namespace Umbraco.Core.Runtime throw; } - var result = InsertLockRecord(tempId); //we change the row to a random Id to signal other MainDom to shutdown + var result = InsertLockRecord(tempId, db); //we change the row to a random Id to signal other MainDom to shutdown if (result == RecordPersistenceType.Insert) { // if we've inserted, then there was no MainDom so we can instantly acquire - // TODO: see the other TODO, could we just delete the row and that would indicate that we - // are MainDom? then we don't leave any orphan rows behind. - - InsertLockRecord(_lockId); // so update with our appdomain id + InsertLockRecord(_lockId, db); // so update with our appdomain id _logger.Debug("Acquired with ID {LockId}", _lockId); return true; } @@ -100,16 +98,12 @@ namespace Umbraco.Core.Runtime } catch (Exception ex) { - ResetDatabase(); // unexpected _logger.Error(ex, "Unexpected error, cannot acquire MainDom"); _hasError = true; return false; } - finally - { - db?.CompleteTransaction(); - } + return await WaitForExistingAsync(tempId, millisecondsTimeout); } @@ -160,12 +154,10 @@ namespace Umbraco.Core.Runtime if (_cancellationTokenSource.IsCancellationRequested) return; - var db = GetDatabase(); - + using var db = _dbFactory.CreateDatabase(); + using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted); try { - db.BeginTransaction(IsolationLevel.ReadCommitted); - // get a read lock _sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom); @@ -173,7 +165,7 @@ namespace Umbraco.Core.Runtime // we are still the maindom. An empty value might be better because then we won't have any orphan rows // if the app is terminated. Could that work? - if (!IsMainDomValue(_lockId)) + if (!IsMainDomValue(_lockId, db)) { // we are no longer main dom, another one has come online, exit _mainDomChanging = true; @@ -183,7 +175,11 @@ namespace Umbraco.Core.Runtime } catch (Exception ex) { - ResetDatabase(); + // TODO: We need to make this more resilient to Azure Sql and timeout issues and not just exit causing the + // app to restart. We don't wan to restart unless we know there's another appdomain online + + //ResetDatabase(); + // unexpected _logger.Error(ex, "Unexpected error, listening is canceled."); _hasError = true; @@ -191,30 +187,13 @@ namespace Umbraco.Core.Runtime } finally { - db?.CompleteTransaction(); + transaction.Complete(); } } } } - private void ResetDatabase() - { - if (_db.InTransaction) - _db.AbortTransaction(); - _db.Dispose(); - _db = null; - } - - private IUmbracoDatabase GetDatabase() - { - if (_db != null) - return _db; - - _db = _dbFactory.CreateDatabase(); - return _db; - } - /// /// Wait for any existing MainDom to release so we can continue booting /// @@ -227,67 +206,64 @@ namespace Umbraco.Core.Runtime return Task.Run(() => { - var db = GetDatabase(); + // ensure this is disposed when this thread ends + using var db = _dbFactory.CreateDatabase(); + var watch = new Stopwatch(); watch.Start(); - while(true) + while (true) { // poll very often, we need to take over as fast as we can - Thread.Sleep(100); + Thread.Sleep(500); - try + using (var transaction = db.GetTransaction(IsolationLevel.ReadCommitted)) { - db.BeginTransaction(IsolationLevel.ReadCommitted); - - // get a read lock - _sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom); - - // the row - var mainDomRows = db.Fetch("SELECT * FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); - - if (mainDomRows.Count == 0 || mainDomRows[0].Value == updatedTempId) + try { - // the other main dom has updated our record - // Or the other maindom shutdown super fast and just deleted the record - // which indicates that we - // can acquire it and it has shutdown. + // get a read lock + _sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom); - _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); + // the row + var mainDomRows = db.Fetch("SELECT * FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); - // so now we update the row with our appdomain id - InsertLockRecord(_lockId); - _logger.Debug("Acquired with ID {LockId}", _lockId); - return true; + if (mainDomRows.Count == 0 || mainDomRows[0].Value == updatedTempId) + { + // the other main dom has updated our record + // Or the other maindom shutdown super fast and just deleted the record + // which indicates that we + // can acquire it and it has shutdown. + + _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); + + // so now we update the row with our appdomain id + InsertLockRecord(_lockId, db); + _logger.Debug("Acquired with ID {LockId}", _lockId); + transaction.Complete(); + return true; + } + else if (mainDomRows.Count == 1 && !mainDomRows[0].Value.StartsWith(tempId)) + { + // in this case, the prefixed ID is different which means + // another new AppDomain has come online and is wanting to take over. In that case, we will not + // acquire. + + _logger.Debug("Cannot acquire, another booting application detected."); + return false; + } } - else if (mainDomRows.Count == 1 && !mainDomRows[0].Value.StartsWith(tempId)) + catch (Exception ex) { - // in this case, the prefixed ID is different which means - // another new AppDomain has come online and is wanting to take over. In that case, we will not - // acquire. - - _logger.Debug("Cannot acquire, another booting application detected."); - - return false; - } - } - catch (Exception ex) - { - ResetDatabase(); - - if (IsLockTimeoutException(ex)) - { - _logger.Error(ex, "Sql timeout occurred, waiting for existing MainDom is canceled."); + if (IsLockTimeoutException(ex)) + { + _logger.Error(ex, "Sql timeout occurred, waiting for existing MainDom is canceled."); + _hasError = true; + return false; + } + // unexpected + _logger.Error(ex, "Unexpected error, waiting for existing MainDom is canceled."); _hasError = true; return false; } - // unexpected - _logger.Error(ex, "Unexpected error, waiting for existing MainDom is canceled."); - _hasError = true; - return false; - } - finally - { - db?.CompleteTransaction(); } if (watch.ElapsedMilliseconds >= millisecondsTimeout) @@ -301,21 +277,22 @@ namespace Umbraco.Core.Runtime _logger.Debug("Timeout elapsed, assuming orphan row, acquiring MainDom."); + using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted); + try { - db.BeginTransaction(IsolationLevel.ReadCommitted); - _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); // so now we update the row with our appdomain id - InsertLockRecord(_lockId); + InsertLockRecord(_lockId, db); _logger.Debug("Acquired with ID {LockId}", _lockId); + + transaction.Complete(); + return true; } catch (Exception ex) { - ResetDatabase(); - if (IsLockTimeoutException(ex)) { // something is wrong, we cannot acquire, not much we can do @@ -327,10 +304,6 @@ namespace Umbraco.Core.Runtime _hasError = true; return false; } - finally - { - db?.CompleteTransaction(); - } } } }, _cancellationTokenSource.Token); @@ -339,9 +312,8 @@ namespace Umbraco.Core.Runtime /// /// Inserts or updates the key/value row /// - private RecordPersistenceType InsertLockRecord(string id) + private RecordPersistenceType InsertLockRecord(string id, IUmbracoDatabase db) { - var db = GetDatabase(); return db.InsertOrUpdate(new KeyValueDto { Key = MainDomKey, @@ -354,9 +326,8 @@ namespace Umbraco.Core.Runtime /// Checks if the DB row value is equals the value /// /// - private bool IsMainDomValue(string val) + private bool IsMainDomValue(string val, IUmbracoDatabase db) { - var db = GetDatabase(); return db.ExecuteScalar("SELECT COUNT(*) FROM umbracoKeyValue WHERE [key] = @key AND [value] = @val", new { key = MainDomKey, val = val }) == 1; } @@ -385,7 +356,10 @@ namespace Umbraco.Core.Runtime if (_dbFactory.Configured) { - var db = GetDatabase(); + // ensure this is disposed when this thread ends + using var db = _dbFactory.CreateDatabase(); + using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted); + try { db.BeginTransaction(IsolationLevel.ReadCommitted); @@ -412,14 +386,12 @@ namespace Umbraco.Core.Runtime } catch (Exception ex) { - ResetDatabase(); _logger.Error(ex, "Unexpected error during dipsose."); _hasError = true; } finally { - db?.CompleteTransaction(); - ResetDatabase(); + transaction.Complete(); } } } From 65101beaf668697300b2a3bd691679e67dec3a73 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2020 13:00:30 +1000 Subject: [PATCH 24/37] transactions for sqlmaindom --- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 24 ++++++++++++++-------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index 8e2e688d66..049a7a9400 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -103,6 +103,10 @@ namespace Umbraco.Core.Runtime _hasError = true; return false; } + finally + { + transaction.Complete(); + } return await WaitForExistingAsync(tempId, millisecondsTimeout); @@ -237,8 +241,7 @@ namespace Umbraco.Core.Runtime // so now we update the row with our appdomain id InsertLockRecord(_lockId, db); - _logger.Debug("Acquired with ID {LockId}", _lockId); - transaction.Complete(); + _logger.Debug("Acquired with ID {LockId}", _lockId); return true; } else if (mainDomRows.Count == 1 && !mainDomRows[0].Value.StartsWith(tempId)) @@ -264,6 +267,10 @@ namespace Umbraco.Core.Runtime _hasError = true; return false; } + finally + { + transaction.Complete(); + } } if (watch.ElapsedMilliseconds >= millisecondsTimeout) @@ -286,9 +293,6 @@ namespace Umbraco.Core.Runtime // so now we update the row with our appdomain id InsertLockRecord(_lockId, db); _logger.Debug("Acquired with ID {LockId}", _lockId); - - transaction.Complete(); - return true; } catch (Exception ex) @@ -304,6 +308,10 @@ namespace Umbraco.Core.Runtime _hasError = true; return false; } + finally + { + transaction.Complete(); + } } } }, _cancellationTokenSource.Token); @@ -362,8 +370,6 @@ namespace Umbraco.Core.Runtime try { - db.BeginTransaction(IsolationLevel.ReadCommitted); - // get a write lock _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); @@ -376,12 +382,12 @@ namespace Umbraco.Core.Runtime if (_mainDomChanging) { _logger.Debug("Releasing MainDom, updating row, new application is booting."); - db.Execute($"UPDATE umbracoKeyValue SET [value] = [value] + '{UpdatedSuffix}' WHERE [key] = @key", new { key = MainDomKey }); + var count = db.Execute($"UPDATE umbracoKeyValue SET [value] = [value] + '{UpdatedSuffix}' WHERE [key] = @key", new { key = MainDomKey }); } else { _logger.Debug("Releasing MainDom, deleting row, application is shutting down."); - db.Execute("DELETE FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); + var count = db.Execute("DELETE FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); } } catch (Exception ex) From 53db2df3907eabf76c1117a7d40a260ec1dcfb58 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2020 13:01:39 +1000 Subject: [PATCH 25/37] Fix for PerformScheduledPublishInternal, don't use yield returns within a using! this will not work and transactions/connections will be lost --- .../Services/Implement/ContentService.cs | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index 1558b0170b..6b7a858d3e 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -1369,6 +1369,8 @@ namespace Umbraco.Core.Services.Implement { var evtMsgs = EventMessagesFactory.Get(); + var results = new List(); + using (var scope = ScopeProvider.CreateScope()) { scope.WriteLock(Constants.Locks.ContentTree); @@ -1377,7 +1379,6 @@ namespace Umbraco.Core.Services.Implement foreach (var d in _documentRepository.GetContentForRelease(date)) { - PublishResult result; if (d.ContentType.VariesByCulture()) { //find which cultures have pending schedules @@ -1391,7 +1392,10 @@ namespace Umbraco.Core.Services.Implement var saveEventArgs = new ContentSavingEventArgs(d, evtMsgs); if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving))) - yield return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d); + { + results.Add(new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d)); + continue; + } var publishing = true; foreach (var culture in pendingCultures) @@ -1413,6 +1417,8 @@ namespace Umbraco.Core.Services.Implement if (!publishing) break; // no point continuing } + PublishResult result; + if (d.Trashed) result = new PublishResult(PublishResultType.FailedPublishIsTrashed, evtMsgs, d); else if (!publishing) @@ -1420,31 +1426,30 @@ namespace Umbraco.Core.Services.Implement else result = CommitDocumentChangesInternal(scope, d, saveEventArgs, allLangs, d.WriterId); - if (result.Success == false) Logger.Error(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); - yield return result; + results.Add(result); } else { //Clear this schedule d.ContentSchedule.Clear(ContentScheduleAction.Release, date); - result = d.Trashed + var result = d.Trashed ? new PublishResult(PublishResultType.FailedPublishIsTrashed, evtMsgs, d) : SaveAndPublish(d, userId: d.WriterId); if (result.Success == false) Logger.Error(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); - yield return result; + results.Add(result); } } foreach (var d in _documentRepository.GetContentForExpiration(date)) { - PublishResult result; + if (d.ContentType.VariesByCulture()) { //find which cultures have pending schedules @@ -1458,7 +1463,10 @@ namespace Umbraco.Core.Services.Implement var saveEventArgs = new ContentSavingEventArgs(d, evtMsgs); if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving))) - yield return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d); + { + results.Add(new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d)); + continue; + } foreach (var c in pendingCultures) { @@ -1468,20 +1476,20 @@ namespace Umbraco.Core.Services.Implement d.UnpublishCulture(c); } - result = CommitDocumentChangesInternal(scope, d, saveEventArgs, allLangs, d.WriterId); + var result = CommitDocumentChangesInternal(scope, d, saveEventArgs, allLangs, d.WriterId); if (result.Success == false) Logger.Error(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); - yield return result; + results.Add(result); } else { //Clear this schedule d.ContentSchedule.Clear(ContentScheduleAction.Expire, date); - result = Unpublish(d, userId: d.WriterId); + var result = Unpublish(d, userId: d.WriterId); if (result.Success == false) Logger.Error(null, "Failed to unpublish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); - yield return result; + results.Add(result); } @@ -1491,6 +1499,8 @@ namespace Umbraco.Core.Services.Implement scope.Complete(); } + + return results; } // utility 'PublishCultures' func used by SaveAndPublishBranch From 651756d96a673316b0cb2cc8a70975423714ab12 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2020 13:39:27 +1000 Subject: [PATCH 26/37] Ensure we don't shutdown MainDom if there is an error while listening, only shutdown if the appdomain is triggered to shutdown, else we'll keep listening/logging --- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 54 +++++++++------------- 1 file changed, 23 insertions(+), 31 deletions(-) diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index 049a7a9400..dc928ed440 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -6,7 +6,6 @@ using System.Linq; using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; -using System.Web; using Umbraco.Core.Logging; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; @@ -21,12 +20,11 @@ namespace Umbraco.Core.Runtime private const string MainDomKeyPrefix = "Umbraco.Core.Runtime.SqlMainDom"; private const string UpdatedSuffix = "_updated"; private readonly ILogger _logger; - private IUmbracoDatabase _db; private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); private SqlServerSyntaxProvider _sqlServerSyntax = new SqlServerSyntaxProvider(); private bool _mainDomChanging = false; private readonly UmbracoDatabaseFactory _dbFactory; - private bool _hasError; + private bool _errorDuringAcquiring; private object _locker = new object(); public SqlMainDomLock(ILogger logger) @@ -68,14 +66,12 @@ namespace Umbraco.Core.Runtime // wait to get a write lock _sqlServerSyntax.WriteLock(db, TimeSpan.FromMilliseconds(millisecondsTimeout), Constants.Locks.MainDom); } - catch (Exception ex) + catch(SqlException ex) { if (IsLockTimeoutException(ex)) { - // TODO: Do we want to retry? We haven't seen any timeout exceptions here so not sure it's important at this stage - _logger.Error(ex, "Sql timeout occurred, could not acquire MainDom."); - _hasError = true; + _errorDuringAcquiring = true; return false; } @@ -100,7 +96,7 @@ namespace Umbraco.Core.Runtime { // unexpected _logger.Error(ex, "Unexpected error, cannot acquire MainDom"); - _hasError = true; + _errorDuringAcquiring = true; return false; } finally @@ -114,7 +110,7 @@ namespace Umbraco.Core.Runtime public Task ListenAsync() { - if (_hasError) + if (_errorDuringAcquiring) { _logger.Warn("Could not acquire MainDom, listening is canceled."); return Task.CompletedTask; @@ -140,8 +136,9 @@ namespace Umbraco.Core.Runtime { while (true) { - // poll every 1 second - Thread.Sleep(1000); + // poll every 1.5 second + // local testing shows the actual query to be executed from client/server is approx 300ms but would change depending on environment/IO + Thread.Sleep(2000); if (!_dbFactory.Configured) { @@ -165,10 +162,6 @@ namespace Umbraco.Core.Runtime // get a read lock _sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom); - // TODO: We could in theory just check if the main dom row doesn't exist, that could indicate that - // we are still the maindom. An empty value might be better because then we won't have any orphan rows - // if the app is terminated. Could that work? - if (!IsMainDomValue(_lockId, db)) { // we are no longer main dom, another one has come online, exit @@ -179,15 +172,14 @@ namespace Umbraco.Core.Runtime } catch (Exception ex) { - // TODO: We need to make this more resilient to Azure Sql and timeout issues and not just exit causing the - // app to restart. We don't wan to restart unless we know there's another appdomain online + // unexpected, if this occurs MainDom will be shutdown! + _logger.Error(ex, "Unexpected error during listening."); - //ResetDatabase(); + // We need to keep on listening unless we've been notified by our own AppDomain to shutdown since + // we don't want to shutdown resources controlled by MainDom inadvertently. We'll just keep listening otherwise. + if (_cancellationTokenSource.IsCancellationRequested) + return; - // unexpected - _logger.Error(ex, "Unexpected error, listening is canceled."); - _hasError = true; - return; } finally { @@ -218,7 +210,8 @@ namespace Umbraco.Core.Runtime while (true) { // poll very often, we need to take over as fast as we can - Thread.Sleep(500); + // local testing shows the actual query to be executed from client/server is approx 300ms but would change depending on environment/IO + Thread.Sleep(1000); using (var transaction = db.GetTransaction(IsolationLevel.ReadCommitted)) { @@ -256,15 +249,15 @@ namespace Umbraco.Core.Runtime } catch (Exception ex) { - if (IsLockTimeoutException(ex)) + if (IsLockTimeoutException(ex as SqlException)) { _logger.Error(ex, "Sql timeout occurred, waiting for existing MainDom is canceled."); - _hasError = true; + _errorDuringAcquiring = true; return false; } // unexpected _logger.Error(ex, "Unexpected error, waiting for existing MainDom is canceled."); - _hasError = true; + _errorDuringAcquiring = true; return false; } finally @@ -297,15 +290,15 @@ namespace Umbraco.Core.Runtime } catch (Exception ex) { - if (IsLockTimeoutException(ex)) + if (IsLockTimeoutException(ex as SqlException)) { // something is wrong, we cannot acquire, not much we can do _logger.Error(ex, "Sql timeout occurred, could not forcibly acquire MainDom."); - _hasError = true; + _errorDuringAcquiring = true; return false; } _logger.Error(ex, "Unexpected error, could not forcibly acquire MainDom."); - _hasError = true; + _errorDuringAcquiring = true; return false; } finally @@ -345,7 +338,7 @@ namespace Umbraco.Core.Runtime /// /// /// - private bool IsLockTimeoutException(Exception exception) => exception is SqlException sqlException && sqlException.Number == 1222; + private bool IsLockTimeoutException(SqlException sqlException) => sqlException?.Number == 1222; #region IDisposable Support private bool _disposedValue = false; // To detect redundant calls @@ -393,7 +386,6 @@ namespace Umbraco.Core.Runtime catch (Exception ex) { _logger.Error(ex, "Unexpected error during dipsose."); - _hasError = true; } finally { From a947fa31211a550a1780c4732981db8b2c6f00c1 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2020 14:51:35 +1000 Subject: [PATCH 27/37] comments --- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index dc928ed440..eb053933da 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -136,7 +136,7 @@ namespace Umbraco.Core.Runtime { while (true) { - // poll every 1.5 second + // poll every couple of seconds // local testing shows the actual query to be executed from client/server is approx 300ms but would change depending on environment/IO Thread.Sleep(2000); From 7819d1acf55a48a023c3d773eae36f6940ba7be6 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2020 14:53:53 +1000 Subject: [PATCH 28/37] comments --- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index eb053933da..fe6acd5485 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -172,7 +172,6 @@ namespace Umbraco.Core.Runtime } catch (Exception ex) { - // unexpected, if this occurs MainDom will be shutdown! _logger.Error(ex, "Unexpected error during listening."); // We need to keep on listening unless we've been notified by our own AppDomain to shutdown since From 759016185843164cde10cace4b787a8fe46b7624 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2020 14:54:26 +1000 Subject: [PATCH 29/37] comments --- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index fe6acd5485..f9d1889442 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -201,7 +201,7 @@ namespace Umbraco.Core.Runtime return Task.Run(() => { - // ensure this is disposed when this thread ends + using var db = _dbFactory.CreateDatabase(); var watch = new Stopwatch(); @@ -356,7 +356,6 @@ namespace Umbraco.Core.Runtime if (_dbFactory.Configured) { - // ensure this is disposed when this thread ends using var db = _dbFactory.CreateDatabase(); using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted); From b80dc8f34b1cc8289bcbda74519fa09f5cf8ddbf Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2020 15:07:12 +1000 Subject: [PATCH 30/37] readability --- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 192 +++++++++++---------- 1 file changed, 102 insertions(+), 90 deletions(-) diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index f9d1889442..209b99aef1 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -201,7 +201,6 @@ namespace Umbraco.Core.Runtime return Task.Run(() => { - using var db = _dbFactory.CreateDatabase(); var watch = new Stopwatch(); @@ -212,103 +211,116 @@ namespace Umbraco.Core.Runtime // local testing shows the actual query to be executed from client/server is approx 300ms but would change depending on environment/IO Thread.Sleep(1000); - using (var transaction = db.GetTransaction(IsolationLevel.ReadCommitted)) - { - try - { - // get a read lock - _sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom); - - // the row - var mainDomRows = db.Fetch("SELECT * FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); - - if (mainDomRows.Count == 0 || mainDomRows[0].Value == updatedTempId) - { - // the other main dom has updated our record - // Or the other maindom shutdown super fast and just deleted the record - // which indicates that we - // can acquire it and it has shutdown. - - _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); - - // so now we update the row with our appdomain id - InsertLockRecord(_lockId, db); - _logger.Debug("Acquired with ID {LockId}", _lockId); - return true; - } - else if (mainDomRows.Count == 1 && !mainDomRows[0].Value.StartsWith(tempId)) - { - // in this case, the prefixed ID is different which means - // another new AppDomain has come online and is wanting to take over. In that case, we will not - // acquire. - - _logger.Debug("Cannot acquire, another booting application detected."); - return false; - } - } - catch (Exception ex) - { - if (IsLockTimeoutException(ex as SqlException)) - { - _logger.Error(ex, "Sql timeout occurred, waiting for existing MainDom is canceled."); - _errorDuringAcquiring = true; - return false; - } - // unexpected - _logger.Error(ex, "Unexpected error, waiting for existing MainDom is canceled."); - _errorDuringAcquiring = true; - return false; - } - finally - { - transaction.Complete(); - } - } + var acquired = TryAcquire(db, tempId, updatedTempId); + if (acquired.HasValue) + return acquired.Value; if (watch.ElapsedMilliseconds >= millisecondsTimeout) { - // if the timeout has elapsed, it either means that the other main dom is taking too long to shutdown, - // or it could mean that the previous appdomain was terminated and didn't clear out the main dom SQL row - // and it's just been left as an orphan row. - // There's really know way of knowing unless we are constantly updating the row for the current maindom - // which isn't ideal. - // So... we're going to 'just' take over, if the writelock works then we'll assume we're ok - - _logger.Debug("Timeout elapsed, assuming orphan row, acquiring MainDom."); - - using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted); - - try - { - _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); - - // so now we update the row with our appdomain id - InsertLockRecord(_lockId, db); - _logger.Debug("Acquired with ID {LockId}", _lockId); - return true; - } - catch (Exception ex) - { - if (IsLockTimeoutException(ex as SqlException)) - { - // something is wrong, we cannot acquire, not much we can do - _logger.Error(ex, "Sql timeout occurred, could not forcibly acquire MainDom."); - _errorDuringAcquiring = true; - return false; - } - _logger.Error(ex, "Unexpected error, could not forcibly acquire MainDom."); - _errorDuringAcquiring = true; - return false; - } - finally - { - transaction.Complete(); - } + return AcquireWhenMaxWaitTimeElapsed(db); } } }, _cancellationTokenSource.Token); } + private bool? TryAcquire(IUmbracoDatabase db, string tempId, string updatedTempId) + { + using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted); + + try + { + // get a read lock + _sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom); + + // the row + var mainDomRows = db.Fetch("SELECT * FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); + + if (mainDomRows.Count == 0 || mainDomRows[0].Value == updatedTempId) + { + // the other main dom has updated our record + // Or the other maindom shutdown super fast and just deleted the record + // which indicates that we + // can acquire it and it has shutdown. + + _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); + + // so now we update the row with our appdomain id + InsertLockRecord(_lockId, db); + _logger.Debug("Acquired with ID {LockId}", _lockId); + return true; + } + else if (mainDomRows.Count == 1 && !mainDomRows[0].Value.StartsWith(tempId)) + { + // in this case, the prefixed ID is different which means + // another new AppDomain has come online and is wanting to take over. In that case, we will not + // acquire. + + _logger.Debug("Cannot acquire, another booting application detected."); + return false; + } + } + catch (Exception ex) + { + if (IsLockTimeoutException(ex as SqlException)) + { + _logger.Error(ex, "Sql timeout occurred, waiting for existing MainDom is canceled."); + _errorDuringAcquiring = true; + return false; + } + // unexpected + _logger.Error(ex, "Unexpected error, waiting for existing MainDom is canceled."); + _errorDuringAcquiring = true; + return false; + } + finally + { + transaction.Complete(); + } + + return null; // continue + } + + private bool AcquireWhenMaxWaitTimeElapsed(IUmbracoDatabase db) + { + // if the timeout has elapsed, it either means that the other main dom is taking too long to shutdown, + // or it could mean that the previous appdomain was terminated and didn't clear out the main dom SQL row + // and it's just been left as an orphan row. + // There's really know way of knowing unless we are constantly updating the row for the current maindom + // which isn't ideal. + // So... we're going to 'just' take over, if the writelock works then we'll assume we're ok + + _logger.Debug("Timeout elapsed, assuming orphan row, acquiring MainDom."); + + using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted); + + try + { + _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); + + // so now we update the row with our appdomain id + InsertLockRecord(_lockId, db); + _logger.Debug("Acquired with ID {LockId}", _lockId); + return true; + } + catch (Exception ex) + { + if (IsLockTimeoutException(ex as SqlException)) + { + // something is wrong, we cannot acquire, not much we can do + _logger.Error(ex, "Sql timeout occurred, could not forcibly acquire MainDom."); + _errorDuringAcquiring = true; + return false; + } + _logger.Error(ex, "Unexpected error, could not forcibly acquire MainDom."); + _errorDuringAcquiring = true; + return false; + } + finally + { + transaction.Complete(); + } + } + /// /// Inserts or updates the key/value row /// From f0dea44b4de0bae48a68bb7d963d316f29363d79 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2020 15:47:26 +1000 Subject: [PATCH 31/37] Adds Load Test controller to test data project (instead of just being stored in my personal repo) --- src/Umbraco.TestData/LoadTestController.cs | 371 +++++++++++++++++++ src/Umbraco.TestData/Umbraco.TestData.csproj | 1 + src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 7 +- 3 files changed, 378 insertions(+), 1 deletion(-) create mode 100644 src/Umbraco.TestData/LoadTestController.cs diff --git a/src/Umbraco.TestData/LoadTestController.cs b/src/Umbraco.TestData/LoadTestController.cs new file mode 100644 index 0000000000..97665dd084 --- /dev/null +++ b/src/Umbraco.TestData/LoadTestController.cs @@ -0,0 +1,371 @@ +using System; +using System.Threading; +using System.Linq; +using System.Web.Mvc; +using Umbraco.Core.Services; +using Umbraco.Core.Models; +using System.Web; +using System.Web.Hosting; +using System.Web.Routing; +using System.Diagnostics; +using Umbraco.Core.Composing; +using System.Configuration; + +// see https://github.com/Shazwazza/UmbracoScripts/tree/master/src/LoadTesting + +namespace Umbraco.TestData +{ + public class LoadTestController : Controller + { + public LoadTestController(ServiceContext serviceContext) + { + _serviceContext = serviceContext; + } + + private static readonly Random _random = new Random(); + private static readonly object _locko = new object(); + + private static volatile int _containerId = -1; + + private const string _containerAlias = "LoadTestContainer"; + private const string _contentAlias = "LoadTestContent"; + private const int _textboxDefinitionId = -88; + private const int _maxCreate = 1000; + + private static readonly string HeadHtml = @" + + LoadTest + + + +
+

LoadTest

+
" + System.Configuration.ConfigurationManager.AppSettings["umbracoConfigurationStatus"] + @"
+
+"; + + private const string FootHtml = @" +"; + + private static readonly string _containerTemplateText = @" +@inherits Umbraco.Web.Mvc.UmbracoViewPage +@{ + Layout = null; + var container = Umbraco.ContentAtRoot().OfTypes(""" + _containerAlias + @""").FirstOrDefault(); + var contents = container.Children().ToArray(); + var groups = contents.GroupBy(x => x.Value(""origin"")); + var id = contents.Length > 0 ? contents[0].Id : -1; + var wurl = Request.QueryString[""u""] == ""1""; + var missing = contents.Length > 0 && contents[contents.Length - 1].Id - contents[0].Id >= contents.Length; +} +" + HeadHtml + @" +
+@contents.Length items +
    +@foreach (var group in groups) +{ +
  • @group.Key: @group.Count()
  • +} +
+
+@foreach (var content in contents) +{ + while (content.Id > id) + { +
@id :: MISSING
+ id++; + } + if (wurl) + { +
@content.Id :: @content.Name :: @content.Url
+ } + else + { +
@content.Id :: @content.Name
+ } id++; +} +
+" + FootHtml; + private readonly ServiceContext _serviceContext; + + private ActionResult ContentHtml(string s) + { + return Content(HeadHtml + s + FootHtml); + } + + public ActionResult Index() + { + var res = EnsureInitialize(); + if (res != null) return res; + + var html = @"Welcome. You can: + +"; + + return ContentHtml(html); + } + + private ActionResult EnsureInitialize() + { + if (_containerId > 0) return null; + + lock (_locko) + { + if (_containerId > 0) return null; + + var contentTypeService = _serviceContext.ContentTypeService; + var contentType = contentTypeService.Get(_contentAlias); + if (contentType == null) + return ContentHtml("Not installed, first you must install."); + + var containerType = contentTypeService.Get(_containerAlias); + if (containerType == null) + return ContentHtml("Panic! Container type is missing."); + + var contentService = _serviceContext.ContentService; + var container = contentService.GetPagedOfType(containerType.Id, 0, 100, out _, null).FirstOrDefault(); + if (container == null) + return ContentHtml("Panic! Container is missing."); + + _containerId = container.Id; + return null; + } + } + + public ActionResult Install() + { + var dataTypeService = _serviceContext.DataTypeService; + + //var dataType = dataTypeService.GetAll(Constants.DataTypes.DefaultContentListView); + + + //if (!dict.ContainsKey("pageSize")) dict["pageSize"] = new PreValue("10"); + //dict["pageSize"].Value = "200"; + //dataTypeService.SavePreValues(dataType, dict); + + var contentTypeService = _serviceContext.ContentTypeService; + + var contentType = new ContentType(-1) + { + Alias = _contentAlias, + Name = "LoadTest Content", + Description = "Content for LoadTest", + Icon = "icon-document" + }; + var def = _serviceContext.DataTypeService.GetDataType(_textboxDefinitionId); + contentType.AddPropertyType(new PropertyType(def) + { + Name = "Origin", + Alias = "origin", + Description = "The origin of the content.", + }); + contentTypeService.Save(contentType); + + var containerTemplate = ImportTemplate(_serviceContext, + "LoadTestContainer", "LoadTestContainer", _containerTemplateText); + + var containerType = new ContentType(-1) + { + Alias = _containerAlias, + Name = "LoadTest Container", + Description = "Container for LoadTest content", + Icon = "icon-document", + AllowedAsRoot = true, + IsContainer = true + }; + containerType.AllowedContentTypes = containerType.AllowedContentTypes.Union(new[] + { + new ContentTypeSort(new Lazy(() => contentType.Id), 0, contentType.Alias), + }); + containerType.AllowedTemplates = containerType.AllowedTemplates.Union(new[] { containerTemplate }); + containerType.SetDefaultTemplate(containerTemplate); + contentTypeService.Save(containerType); + + var contentService = _serviceContext.ContentService; + var content = contentService.Create("LoadTestContainer", -1, _containerAlias); + contentService.SaveAndPublish(content); + + return ContentHtml("Installed."); + } + + public ActionResult Create(int n = 1, int r = 0, string o = null) + { + var res = EnsureInitialize(); + if (res != null) return res; + + if (r < 0) r = 0; + if (r > 100) r = 100; + var restart = GetRandom(0, 100) > (100 - r); + + var contentService = _serviceContext.ContentService; + + if (n < 1) n = 1; + if (n > _maxCreate) n = _maxCreate; + for (int i = 0; i < n; i++) + { + var name = Guid.NewGuid().ToString("N").ToUpper() + "-" + (restart ? "R" : "X") + "-" + o; + var content = contentService.Create(name, _containerId, _contentAlias); + content.SetValue("origin", o); + contentService.SaveAndPublish(content); + } + + if (restart) + DoRestart(); + + return ContentHtml("Created " + n + " content" + + (restart ? ", and restarted" : "") + + "."); + } + + private int GetRandom(int minValue, int maxValue) + { + lock (_locko) + { + return _random.Next(minValue, maxValue); + } + } + + public ActionResult Clear() + { + var res = EnsureInitialize(); + if (res != null) return res; + + var contentType = _serviceContext.ContentTypeService.Get(_contentAlias); + _serviceContext.ContentService.DeleteOfType(contentType.Id); + + return ContentHtml("Cleared."); + } + + private void DoRestart() + { + HttpContext.User = null; + System.Web.HttpContext.Current.User = null; + Thread.CurrentPrincipal = null; + HttpRuntime.UnloadAppDomain(); + } + + public ActionResult Restart() + { + DoRestart(); + + return ContentHtml("Restarted."); + } + + public ActionResult Die() + { + var timer = new System.Threading.Timer(_ => + { + throw new Exception("die!"); + }); + timer.Change(100, 0); + + return ContentHtml("Dying."); + } + + public ActionResult Domains() + { + var currentDomain = AppDomain.CurrentDomain; + var currentName = currentDomain.FriendlyName; + var pos = currentName.IndexOf('-'); + if (pos > 0) currentName = currentName.Substring(0, pos); + + var text = new System.Text.StringBuilder(); + text.Append("
Process ID: " + Process.GetCurrentProcess().Id + "
"); + text.Append("
"); + text.Append("
IIS Site: " + HostingEnvironment.ApplicationHost.GetSiteName() + "
"); + text.Append("
App ID: " + currentName + "
"); + //text.Append("
AppPool: " + Zbu.WebManagement.AppPoolHelper.GetCurrentApplicationPoolName() + "
"); + text.Append("
"); + + text.Append("
Domains:
    "); + text.Append("
  • Not implemented.
  • "); + /* + foreach (var domain in Zbu.WebManagement.AppDomainHelper.GetAppDomains().OrderBy(x => x.Id)) + { + var name = domain.FriendlyName; + pos = name.IndexOf('-'); + if (pos > 0) name = name.Substring(0, pos); + text.Append("
  • " + +"[" + domain.Id + "] " + name + + (domain.IsDefaultAppDomain() ? " (default)" : "") + + (domain.Id == currentDomain.Id ? " (current)" : "") + + "
  • "); + } + */ + text.Append("
"); + + return ContentHtml(text.ToString()); + } + + public ActionResult Recycle() + { + return ContentHtml("Not implemented—please use IIS console."); + } + + private static Template ImportTemplate(ServiceContext svces, string name, string alias, string text, ITemplate master = null) + { + var t = new Template(name, alias) { Content = text }; + if (master != null) + t.SetMasterTemplate(master); + svces.FileService.SaveTemplate(t); + return t; + } + } + + public class TestComponent : IComponent + { + public void Initialize() + { + if (ConfigurationManager.AppSettings["Umbraco.TestData.Enabled"] != "true") + return; + + RouteTable.Routes.MapRoute( + name: "LoadTest", + url: "LoadTest/{action}", + defaults: new + { + controller = "LoadTest", + action = "Index" + }, + namespaces: new[] { "Umbraco.TestData" } + ); + } + + public void Terminate() + { + } + } + + public class TestComposer : ComponentComposer, IUserComposer + { + public override void Compose(Composition composition) + { + base.Compose(composition); + + if (ConfigurationManager.AppSettings["Umbraco.TestData.Enabled"] != "true") + return; + + composition.Register(typeof(LoadTestController), Lifetime.Request); + } + } +} diff --git a/src/Umbraco.TestData/Umbraco.TestData.csproj b/src/Umbraco.TestData/Umbraco.TestData.csproj index d61321ebb8..a3753cc17b 100644 --- a/src/Umbraco.TestData/Umbraco.TestData.csproj +++ b/src/Umbraco.TestData/Umbraco.TestData.csproj @@ -41,6 +41,7 @@ + diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 0de757531a..009659c0e2 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -12,7 +12,8 @@ {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} OnBuildSuccess true - 44331 + + enabled disabled false @@ -126,6 +127,10 @@ {52ac0ba8-a60e-4e36-897b-e8b97a54ed1c} Umbraco.ModelsBuilder.Embedded + + {fb5676ed-7a69-492c-b802-e7b24144c0fc} + Umbraco.TestData + {651e1350-91b6-44b7-bd60-7207006d7003} Umbraco.Web From 384531ea6878c87c86f133c2499c3208382e3b4c Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2020 15:59:35 +1000 Subject: [PATCH 32/37] removes comments and no need for private method --- src/Umbraco.Core/Services/Implement/ContentService.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index 6b7a858d3e..93e7f0a5df 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -1359,13 +1359,7 @@ namespace Umbraco.Core.Services.Implement } /// - public IEnumerable PerformScheduledPublish(DateTime date) - => PerformScheduledPublishInternal(date).ToList(); - - // beware! this method yields results, so the returned IEnumerable *must* be - // enumerated for anything to happen - dangerous, so private + exposed via - // the public method above, which forces ToList(). - private IEnumerable PerformScheduledPublishInternal(DateTime date) + public IEnumerable PerformScheduledPublish(DateTime date) { var evtMsgs = EventMessagesFactory.Get(); From e1757178b9605772d5c4c2fb70e70f918f567f4e Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2020 17:26:31 +1000 Subject: [PATCH 33/37] Fixes our sql azure transient fault detection to be inline with current standards, adds a scope for the health check schedule tasks --- ...SqlAzureTransientErrorDetectionStrategy.cs | 44 ++++++++++++------- .../Scheduling/HealthCheckNotifier.cs | 10 ++++- src/Umbraco.Web/Scheduling/LogScrubber.cs | 3 +- .../Scheduling/ScheduledPublishing.cs | 41 ++++++++--------- .../Scheduling/SchedulerComponent.cs | 2 +- 5 files changed, 57 insertions(+), 43 deletions(-) diff --git a/src/Umbraco.Core/Persistence/FaultHandling/Strategies/SqlAzureTransientErrorDetectionStrategy.cs b/src/Umbraco.Core/Persistence/FaultHandling/Strategies/SqlAzureTransientErrorDetectionStrategy.cs index 849fd35fad..f763594616 100644 --- a/src/Umbraco.Core/Persistence/FaultHandling/Strategies/SqlAzureTransientErrorDetectionStrategy.cs +++ b/src/Umbraco.Core/Persistence/FaultHandling/Strategies/SqlAzureTransientErrorDetectionStrategy.cs @@ -4,6 +4,10 @@ using System.Data.SqlClient; namespace Umbraco.Core.Persistence.FaultHandling.Strategies { + // See https://docs.microsoft.com/en-us/azure/azure-sql/database/troubleshoot-common-connectivity-issues + // Also we could just use the nuget package instead https://www.nuget.org/packages/EnterpriseLibrary.TransientFaultHandling/ ? + // but i guess that's not netcore so we'll just leave it. + /// /// Provides the transient error detection logic for transient faults that are specific to SQL Azure. /// @@ -71,7 +75,7 @@ namespace Umbraco.Core.Persistence.FaultHandling.Strategies /// Determines whether the specified exception represents a transient failure that can be compensated by a retry. /// /// The exception object to be verified. - /// True if the specified exception is considered as transient, otherwise false. + /// true if the specified exception is considered as transient; otherwise, false. public bool IsTransient(Exception ex) { if (ex != null) @@ -97,40 +101,50 @@ namespace Umbraco.Core.Persistence.FaultHandling.Strategies return true; - // SQL Error Code: 40197 - // The service has encountered an error processing your request. Please try again. - case 40197: + // SQL Error Code: 10928 + // Resource ID: %d. The %s limit for the database is %d and has been reached. + case 10928: + // SQL Error Code: 10929 + // Resource ID: %d. The %s minimum guarantee is %d, maximum limit is %d and the current usage for the database is %d. + // However, the server is currently too busy to support requests greater than %d for this database. + case 10929: // SQL Error Code: 10053 // A transport-level error has occurred when receiving results from the server. // An established connection was aborted by the software in your host machine. case 10053: // SQL Error Code: 10054 - // A transport-level error has occurred when sending the request to the server. + // A transport-level error has occurred when sending the request to the server. // (provider: TCP Provider, error: 0 - An existing connection was forcibly closed by the remote host.) case 10054: // SQL Error Code: 10060 - // A network-related or instance-specific error occurred while establishing a connection to SQL Server. - // The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server - // is configured to allow remote connections. (provider: TCP Provider, error: 0 - A connection attempt failed - // because the connected party did not properly respond after a period of time, or established connection failed + // A network-related or instance-specific error occurred while establishing a connection to SQL Server. + // The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server + // is configured to allow remote connections. (provider: TCP Provider, error: 0 - A connection attempt failed + // because the connected party did not properly respond after a period of time, or established connection failed // because connected host has failed to respond.)"} case 10060: + // SQL Error Code: 40197 + // The service has encountered an error processing your request. Please try again. + case 40197: + // SQL Error Code: 40540 + // The service has encountered an error processing your request. Please try again. + case 40540: // SQL Error Code: 40613 - // Database XXXX on server YYYY is not currently available. Please retry the connection later. If the problem persists, contact customer + // Database XXXX on server YYYY is not currently available. Please retry the connection later. If the problem persists, contact customer // support, and provide them the session tracing ID of ZZZZZ. case 40613: // SQL Error Code: 40143 // The service has encountered an error processing your request. Please try again. case 40143: // SQL Error Code: 233 - // The client was unable to establish a connection because of an error during connection initialization process before login. - // Possible causes include the following: the client tried to connect to an unsupported version of SQL Server; the server was too busy - // to accept new connections; or there was a resource limitation (insufficient memory or maximum allowed connections) on the server. + // The client was unable to establish a connection because of an error during connection initialization process before login. + // Possible causes include the following: the client tried to connect to an unsupported version of SQL Server; the server was too busy + // to accept new connections; or there was a resource limitation (insufficient memory or maximum allowed connections) on the server. // (provider: TCP Provider, error: 0 - An existing connection was forcibly closed by the remote host.) case 233: // SQL Error Code: 64 - // A connection was successfully established with the server, but then an error occurred during the login process. - // (provider: TCP Provider, error: 0 - The specified network name is no longer available.) + // A connection was successfully established with the server, but then an error occurred during the login process. + // (provider: TCP Provider, error: 0 - The specified network name is no longer available.) case 64: // DBNETLIB Error Code: 20 // The instance of SQL Server you attempted to connect to does not support encryption. diff --git a/src/Umbraco.Web/Scheduling/HealthCheckNotifier.cs b/src/Umbraco.Web/Scheduling/HealthCheckNotifier.cs index 746bc61e34..9a4a4f0e2c 100644 --- a/src/Umbraco.Web/Scheduling/HealthCheckNotifier.cs +++ b/src/Umbraco.Web/Scheduling/HealthCheckNotifier.cs @@ -5,6 +5,7 @@ using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.Logging; +using Umbraco.Core.Scoping; using Umbraco.Core.Sync; using Umbraco.Web.HealthCheck; @@ -15,16 +16,17 @@ namespace Umbraco.Web.Scheduling private readonly IRuntimeState _runtimeState; private readonly HealthCheckCollection _healthChecks; private readonly HealthCheckNotificationMethodCollection _notifications; + private readonly IScopeProvider _scopeProvider; private readonly IProfilingLogger _logger; public HealthCheckNotifier(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, HealthCheckCollection healthChecks, HealthCheckNotificationMethodCollection notifications, - IRuntimeState runtimeState, - IProfilingLogger logger) + IScopeProvider scopeProvider, IRuntimeState runtimeState, IProfilingLogger logger) : base(runner, delayMilliseconds, periodMilliseconds) { _healthChecks = healthChecks; _notifications = notifications; + _scopeProvider = scopeProvider; _runtimeState = runtimeState; _logger = logger; } @@ -51,6 +53,10 @@ namespace Umbraco.Web.Scheduling return false; // do NOT repeat, going down } + // Ensure we use an explicit scope since we are running on a background thread and plugin health + // checks can be making service/database calls so we want to ensure the CallContext/Ambient scope + // isn't used since that can be problematic. + using (var scope = _scopeProvider.CreateScope()) using (_logger.DebugDuration("Health checks executing", "Health checks complete")) { var healthCheckConfig = Current.Configs.HealthChecks(); diff --git a/src/Umbraco.Web/Scheduling/LogScrubber.cs b/src/Umbraco.Web/Scheduling/LogScrubber.cs index db13a80f9b..ffdb584c7a 100644 --- a/src/Umbraco.Web/Scheduling/LogScrubber.cs +++ b/src/Umbraco.Web/Scheduling/LogScrubber.cs @@ -70,8 +70,7 @@ namespace Umbraco.Web.Scheduling return false; // do NOT repeat, going down } - // running on a background task, and Log.CleanLogs uses the old SqlHelper, - // better wrap in a scope and ensure it's all cleaned up and nothing leaks + // Ensure we use an explicit scope since we are running on a background thread. using (var scope = _scopeProvider.CreateScope()) using (_logger.DebugDuration("Log scrubbing executing", "Log scrubbing complete")) { diff --git a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs index 2e79e40d7a..97afe25e22 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs @@ -2,6 +2,7 @@ using System.Linq; using Umbraco.Core; using Umbraco.Core.Logging; +using Umbraco.Core.Scoping; using Umbraco.Core.Services; using Umbraco.Core.Sync; @@ -55,30 +56,24 @@ namespace Umbraco.Web.Scheduling try { - // ensure we run with an UmbracoContext, because this may run in a background task, - // yet developers may be using the 'current' UmbracoContext in the event handlers - // - // TODO: or maybe not, CacheRefresherComponent already ensures a context when handling events - // - UmbracoContext 'current' needs to be refactored and cleaned up - // - batched messenger should not depend on a current HttpContext - // but then what should be its "scope"? could we attach it to scopes? - // - and we should definitively *not* have to flush it here (should be auto) - // - using (var contextReference = _umbracoContextFactory.EnsureUmbracoContext()) + // We don't need an explicit scope here because PerformScheduledPublish creates it's own scope + // so it's safe as it will create it's own ambient scope. + // Ensure we run with an UmbracoContext, because this will run in a background task, + // and developers may be using the UmbracoContext in the event handlers. + + using var contextReference = _umbracoContextFactory.EnsureUmbracoContext(); + try { - try - { - // run - var result = _contentService.PerformScheduledPublish(DateTime.Now); - foreach (var grouped in result.GroupBy(x => x.Result)) - _logger.Info("Scheduled publishing result: '{StatusCount}' items with status {Status}", grouped.Count(), grouped.Key); - } - finally - { - // if running on a temp context, we have to flush the messenger - if (contextReference.IsRoot && Composing.Current.ServerMessenger is BatchedDatabaseServerMessenger m) - m.FlushBatch(); - } + // run + var result = _contentService.PerformScheduledPublish(DateTime.Now); + foreach (var grouped in result.GroupBy(x => x.Result)) + _logger.Info("Scheduled publishing result: '{StatusCount}' items with status {Status}", grouped.Count(), grouped.Key); + } + finally + { + // if running on a temp context, we have to flush the messenger + if (contextReference.IsRoot && Composing.Current.ServerMessenger is BatchedDatabaseServerMessenger m) + m.FlushBatch(); } } catch (Exception ex) diff --git a/src/Umbraco.Web/Scheduling/SchedulerComponent.cs b/src/Umbraco.Web/Scheduling/SchedulerComponent.cs index a08289186f..f6ce11f939 100644 --- a/src/Umbraco.Web/Scheduling/SchedulerComponent.cs +++ b/src/Umbraco.Web/Scheduling/SchedulerComponent.cs @@ -155,7 +155,7 @@ namespace Umbraco.Web.Scheduling } var periodInMilliseconds = healthCheckConfig.NotificationSettings.PeriodInHours * 60 * 60 * 1000; - var task = new HealthCheckNotifier(_healthCheckRunner, delayInMilliseconds, periodInMilliseconds, healthChecks, notifications, _runtime, logger); + var task = new HealthCheckNotifier(_healthCheckRunner, delayInMilliseconds, periodInMilliseconds, healthChecks, notifications, _scopeProvider, _runtime, logger); _healthCheckRunner.TryAdd(task); return task; } From df61f30393d1107dccf980a5d0511b72b57dc3cf Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2020 18:16:12 +1000 Subject: [PATCH 34/37] refactors scheduled publishing logic - splits into 2x scopes/2x trans, only take a write lock when necessary --- .../Repositories/IDocumentRepository.cs | 5 + .../Implement/DocumentRepository.cs | 31 ++++ .../Services/Implement/ContentService.cs | 158 ++++++++++-------- 3 files changed, 126 insertions(+), 68 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs index fc5382499f..0971b2047a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs @@ -12,6 +12,11 @@ namespace Umbraco.Core.Persistence.Repositories /// void ClearSchedule(DateTime date); + void ClearSchedule(DateTime date, ContentScheduleAction action); + + bool HasContentForExpiration(DateTime date); + bool HasContentForRelease(DateTime date); + /// /// Gets objects having an expiration date before (lower than, or equal to) a specified date. /// diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index ccfa8209fb..a34aadd70f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -1000,6 +1000,37 @@ namespace Umbraco.Core.Persistence.Repositories.Implement Database.Execute(sql); } + /// + public void ClearSchedule(DateTime date, ContentScheduleAction action) + { + var a = action.ToString(); + var sql = Sql().Delete().Where(x => x.Date <= date && x.Action == a); + Database.Execute(sql); + } + + private Sql GetSqlForHasScheduling(ContentScheduleAction action, DateTime date) + { + var template = SqlContext.Templates.Get("Umbraco.Core.DocumentRepository.GetSqlForHasScheduling", tsql => tsql + .SelectCount() + .From() + .Where(x => x.Action == SqlTemplate.Arg("action") && x.Date <= SqlTemplate.Arg("date"))); + + var sql = template.Sql(action.ToString(), date); + return sql; + } + + public bool HasContentForExpiration(DateTime date) + { + var sql = GetSqlForHasScheduling(ContentScheduleAction.Expire, date); + return Database.ExecuteScalar(sql) > 0; + } + + public bool HasContentForRelease(DateTime date) + { + var sql = GetSqlForHasScheduling(ContentScheduleAction.Release, date); + return Database.ExecuteScalar(sql) > 0; + } + /// public IEnumerable GetContentForRelease(DateTime date) { diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index 93e7f0a5df..2a23a1adad 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -30,7 +30,7 @@ namespace Umbraco.Core.Services.Implement private IQuery _queryNotTrashed; //TODO: The non-lazy object should be injected private readonly Lazy _propertyValidationService = new Lazy(() => new PropertyValidationService()); - + #region Constructors @@ -875,7 +875,7 @@ namespace Umbraco.Core.Services.Implement throw new NotSupportedException($"Culture \"{culture}\" is not supported by invariant content types."); } - if(content.Name != null && content.Name.Length > 255) + if (content.Name != null && content.Name.Length > 255) { throw new InvalidOperationException("Name cannot be more than 255 characters in length."); } @@ -1243,7 +1243,7 @@ namespace Umbraco.Core.Services.Implement if (culturesUnpublishing != null) { // This will mean that that we unpublished a mandatory culture or we unpublished the last culture. - + var langs = string.Join(", ", allLangs .Where(x => culturesUnpublishing.InvariantContains(x.IsoCode)) .Select(x => x.CultureName)); @@ -1252,7 +1252,7 @@ namespace Umbraco.Core.Services.Implement if (publishResult == null) throw new PanicException("publishResult == null - should not happen"); - switch(publishResult.Result) + switch (publishResult.Result) { case PublishResultType.FailedPublishMandatoryCultureMissing: //occurs when a mandatory culture was unpublished (which means we tried publishing the document without a mandatory culture) @@ -1266,7 +1266,7 @@ namespace Umbraco.Core.Services.Implement Audit(AuditType.Unpublish, userId, content.Id, "Unpublished (last language unpublished)"); return new PublishResult(PublishResultType.SuccessUnpublishLastCulture, evtMsgs, content); } - + } Audit(AuditType.Unpublish, userId, content.Id); @@ -1286,7 +1286,7 @@ namespace Umbraco.Core.Services.Implement changeType = TreeChangeTypes.RefreshBranch; // whole branch else if (isNew == false && previouslyPublished) changeType = TreeChangeTypes.RefreshNode; // single node - + // invalidate the node/branch if (!branchOne) // for branches, handled by SaveAndPublishBranch @@ -1359,17 +1359,88 @@ namespace Umbraco.Core.Services.Implement } /// - public IEnumerable PerformScheduledPublish(DateTime date) + public IEnumerable PerformScheduledPublish(DateTime date) { + var allLangs = new Lazy>(() => _languageRepository.GetMany().ToList()); var evtMsgs = EventMessagesFactory.Get(); - var results = new List(); - using (var scope = ScopeProvider.CreateScope()) + PerformScheduledPublishingRelease(date, results, evtMsgs, allLangs); + PerformScheduledPublishingExpiration(date, results, evtMsgs, allLangs); + + return results; + } + + private void PerformScheduledPublishingExpiration(DateTime date, List results, EventMessages evtMsgs, Lazy> allLangs) + { + using var scope = ScopeProvider.CreateScope(); + + // do a fast read without any locks since this executes often to see if we even need to proceed + if (_documentRepository.HasContentForExpiration(date)) { + // now take a write lock since we'll be updating scope.WriteLock(Constants.Locks.ContentTree); - var allLangs = _languageRepository.GetMany().ToList(); + foreach (var d in _documentRepository.GetContentForExpiration(date)) + { + if (d.ContentType.VariesByCulture()) + { + //find which cultures have pending schedules + var pendingCultures = d.ContentSchedule.GetPending(ContentScheduleAction.Expire, date) + .Select(x => x.Culture) + .Distinct() + .ToList(); + + if (pendingCultures.Count == 0) + continue; //shouldn't happen but no point in continuing if there's nothing there + + var saveEventArgs = new ContentSavingEventArgs(d, evtMsgs); + if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving))) + { + results.Add(new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d)); + continue; + } + + foreach (var c in pendingCultures) + { + //Clear this schedule for this culture + d.ContentSchedule.Clear(c, ContentScheduleAction.Expire, date); + //set the culture to be published + d.UnpublishCulture(c); + } + + var result = CommitDocumentChangesInternal(scope, d, saveEventArgs, allLangs.Value, d.WriterId); + if (result.Success == false) + Logger.Error(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); + results.Add(result); + + } + else + { + //Clear this schedule + d.ContentSchedule.Clear(ContentScheduleAction.Expire, date); + var result = Unpublish(d, userId: d.WriterId); + if (result.Success == false) + Logger.Error(null, "Failed to unpublish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); + results.Add(result); + } + } + + _documentRepository.ClearSchedule(date, ContentScheduleAction.Expire); + } + + scope.Complete(); + } + + private void PerformScheduledPublishingRelease(DateTime date, List results, EventMessages evtMsgs, Lazy> allLangs) + { + using var scope = ScopeProvider.CreateScope(); + + // do a fast read without any locks since this executes often to see if we even need to proceed + if (_documentRepository.HasContentForRelease(date)) + { + // now take a write lock since we'll be updating + scope.WriteLock(Constants.Locks.ContentTree); foreach (var d in _documentRepository.GetContentForRelease(date)) { @@ -1382,13 +1453,13 @@ namespace Umbraco.Core.Services.Implement .ToList(); if (pendingCultures.Count == 0) - break; //shouldn't happen but no point in continuing if there's nothing there + continue; //shouldn't happen but no point in continuing if there's nothing there var saveEventArgs = new ContentSavingEventArgs(d, evtMsgs); if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving))) { results.Add(new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d)); - continue; + continue; // this document is canceled move next } var publishing = true; @@ -1401,14 +1472,14 @@ namespace Umbraco.Core.Services.Implement //publish the culture values and validate the property values, if validation fails, log the invalid properties so the develeper has an idea of what has failed Property[] invalidProperties = null; - var impact = CultureImpact.Explicit(culture, IsDefaultCulture(allLangs, culture)); + var impact = CultureImpact.Explicit(culture, IsDefaultCulture(allLangs.Value, culture)); var tryPublish = d.PublishCulture(impact) && _propertyValidationService.Value.IsPropertyDataValid(d, out invalidProperties, impact); if (invalidProperties != null && invalidProperties.Length > 0) Logger.Warn("Scheduled publishing will fail for document {DocumentId} and culture {Culture} because of invalid properties {InvalidProperties}", d.Id, culture, string.Join(",", invalidProperties.Select(x => x.Alias))); publishing &= tryPublish; //set the culture to be published - if (!publishing) break; // no point continuing + if (!publishing) continue; // no point continuing } PublishResult result; @@ -1418,7 +1489,7 @@ namespace Umbraco.Core.Services.Implement else if (!publishing) result = new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, d); else - result = CommitDocumentChangesInternal(scope, d, saveEventArgs, allLangs, d.WriterId); + result = CommitDocumentChangesInternal(scope, d, saveEventArgs, allLangs.Value, d.WriterId); if (result.Success == false) Logger.Error(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); @@ -1441,60 +1512,11 @@ namespace Umbraco.Core.Services.Implement } } - foreach (var d in _documentRepository.GetContentForExpiration(date)) - { - - if (d.ContentType.VariesByCulture()) - { - //find which cultures have pending schedules - var pendingCultures = d.ContentSchedule.GetPending(ContentScheduleAction.Expire, date) - .Select(x => x.Culture) - .Distinct() - .ToList(); + _documentRepository.ClearSchedule(date, ContentScheduleAction.Release); - if (pendingCultures.Count == 0) - break; //shouldn't happen but no point in continuing if there's nothing there - - var saveEventArgs = new ContentSavingEventArgs(d, evtMsgs); - if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving))) - { - results.Add(new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d)); - continue; - } - - foreach (var c in pendingCultures) - { - //Clear this schedule for this culture - d.ContentSchedule.Clear(c, ContentScheduleAction.Expire, date); - //set the culture to be published - d.UnpublishCulture(c); - } - - var result = CommitDocumentChangesInternal(scope, d, saveEventArgs, allLangs, d.WriterId); - if (result.Success == false) - Logger.Error(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); - results.Add(result); - - } - else - { - //Clear this schedule - d.ContentSchedule.Clear(ContentScheduleAction.Expire, date); - var result = Unpublish(d, userId: d.WriterId); - if (result.Success == false) - Logger.Error(null, "Failed to unpublish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); - results.Add(result); - } - - - } - - _documentRepository.ClearSchedule(date); - - scope.Complete(); } - return results; + scope.Complete(); } // utility 'PublishCultures' func used by SaveAndPublishBranch @@ -2627,7 +2649,7 @@ namespace Umbraco.Core.Services.Implement // there will be nothing to publish/unpublish. return new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content); } - + // missing mandatory culture = cannot be published var mandatoryCultures = allLangs.Where(x => x.IsMandatory).Select(x => x.IsoCode); @@ -3140,6 +3162,6 @@ namespace Umbraco.Core.Services.Implement #endregion - + } } From 6dfb31f42b6e6e9be2fa70a696bc8649c4735412 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 9 Jul 2020 13:40:35 +1000 Subject: [PATCH 35/37] revert ssl port --- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 009659c0e2..7b3a24512f 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -12,8 +12,7 @@ {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} OnBuildSuccess true - - + 44331 enabled disabled false From 15b903129e313baff3a6e662005b5c39ce635416 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 9 Jul 2020 14:32:48 +1000 Subject: [PATCH 36/37] comments --- src/Umbraco.Core/Services/Implement/ContentService.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index 2a23a1adad..f8b63a2c40 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -1392,7 +1392,7 @@ namespace Umbraco.Core.Services.Implement .ToList(); if (pendingCultures.Count == 0) - continue; //shouldn't happen but no point in continuing if there's nothing there + continue; //shouldn't happen but no point in processing this document if there's nothing there var saveEventArgs = new ContentSavingEventArgs(d, evtMsgs); if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving))) @@ -1453,7 +1453,7 @@ namespace Umbraco.Core.Services.Implement .ToList(); if (pendingCultures.Count == 0) - continue; //shouldn't happen but no point in continuing if there's nothing there + continue; //shouldn't happen but no point in processing this document if there's nothing there var saveEventArgs = new ContentSavingEventArgs(d, evtMsgs); if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving))) @@ -1479,7 +1479,7 @@ namespace Umbraco.Core.Services.Implement d.Id, culture, string.Join(",", invalidProperties.Select(x => x.Alias))); publishing &= tryPublish; //set the culture to be published - if (!publishing) continue; // no point continuing + if (!publishing) continue; // move to next document } PublishResult result; From 0196e55c19c5249fcc0dad013775c36a701b8977 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 9 Jul 2020 16:33:01 +1000 Subject: [PATCH 37/37] Cherry picks fix #7994 for #5151 --- .../Services/UserServiceExtensions.cs | 13 +++++++ src/Umbraco.Examine/ContentValueSetBuilder.cs | 34 +++++++++++++++++-- .../Services/UserServiceTests.cs | 18 ++++++++++ .../UmbracoExamine/IndexInitializer.cs | 16 ++++++--- src/Umbraco.Tests/UmbracoExamine/IndexTest.cs | 10 +++--- .../UmbracoExamine/SearchTests.cs | 2 +- src/Umbraco.Web/Search/ExamineComposer.cs | 3 ++ 7 files changed, 83 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Core/Services/UserServiceExtensions.cs b/src/Umbraco.Core/Services/UserServiceExtensions.cs index 82cab07b25..c365f1ccc2 100644 --- a/src/Umbraco.Core/Services/UserServiceExtensions.cs +++ b/src/Umbraco.Core/Services/UserServiceExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Web.Security; using Umbraco.Core.Models.Membership; @@ -116,5 +117,17 @@ namespace Umbraco.Core.Services var permissionCollection = userService.GetPermissions(user, nodeId); return permissionCollection.SelectMany(c => c.AssignedPermissions).Distinct().ToArray(); } + + internal static IEnumerable GetProfilesById(this IUserService userService, params int[] ids) + { + var fullUsers = userService.GetUsersById(ids); + + return fullUsers.Select(user => + { + var asProfile = user as IProfile; + return asProfile ?? new UserProfile(user.Id, user.Name); + }); + + } } } diff --git a/src/Umbraco.Examine/ContentValueSetBuilder.cs b/src/Umbraco.Examine/ContentValueSetBuilder.cs index 9cbc311639..b8477a9047 100644 --- a/src/Umbraco.Examine/ContentValueSetBuilder.cs +++ b/src/Umbraco.Examine/ContentValueSetBuilder.cs @@ -1,9 +1,13 @@ using Examine; +using System; using System.Collections.Generic; using System.Linq; using Umbraco.Core; +using Umbraco.Core.Composing; using Umbraco.Core.Models; +using Umbraco.Core.Models.Membership; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Scoping; using Umbraco.Core.Services; using Umbraco.Core.Strings; @@ -16,20 +20,46 @@ namespace Umbraco.Examine { private readonly UrlSegmentProviderCollection _urlSegmentProviders; private readonly IUserService _userService; + private readonly IScopeProvider _scopeProvider; + + [Obsolete("Use the other ctor instead")] + public ContentValueSetBuilder(PropertyEditorCollection propertyEditors, + UrlSegmentProviderCollection urlSegmentProviders, + IUserService userService, + bool publishedValuesOnly) + : this(propertyEditors, urlSegmentProviders, userService, Current.ScopeProvider, publishedValuesOnly) + { + } public ContentValueSetBuilder(PropertyEditorCollection propertyEditors, UrlSegmentProviderCollection urlSegmentProviders, IUserService userService, + IScopeProvider scopeProvider, bool publishedValuesOnly) : base(propertyEditors, publishedValuesOnly) { _urlSegmentProviders = urlSegmentProviders; _userService = userService; + _scopeProvider = scopeProvider; } /// public override IEnumerable GetValueSets(params IContent[] content) { + Dictionary creatorIds; + Dictionary writerIds; + + // We can lookup all of the creator/writer names at once which can save some + // processing below instead of one by one. + using (var scope = _scopeProvider.CreateScope()) + { + creatorIds = _userService.GetProfilesById(content.Select(x => x.CreatorId).ToArray()) + .ToDictionary(x => x.Id, x => x); + writerIds = _userService.GetProfilesById(content.Select(x => x.WriterId).ToArray()) + .ToDictionary(x => x.Id, x => x); + scope.Complete(); + } + // TODO: There is a lot of boxing going on here and ultimately all values will be boxed by Lucene anyways // but I wonder if there's a way to reduce the boxing that we have to do or if it will matter in the end since // Lucene will do it no matter what? One idea was to create a `FieldValue` struct which would contain `object`, `object[]`, `ValueType` and `ValueType[]` @@ -58,8 +88,8 @@ namespace Umbraco.Examine {"urlName", urlValue?.Yield() ?? Enumerable.Empty()}, //Always add invariant urlName {"path", c.Path?.Yield() ?? Enumerable.Empty()}, {"nodeType", c.ContentType.Id.ToString().Yield() ?? Enumerable.Empty()}, - {"creatorName", (c.GetCreatorProfile(_userService)?.Name ?? "??").Yield() }, - {"writerName",(c.GetWriterProfile(_userService)?.Name ?? "??").Yield() }, + {"creatorName", (creatorIds.TryGetValue(c.CreatorId, out var creatorProfile) ? creatorProfile.Name : "??").Yield() }, + {"writerName", (writerIds.TryGetValue(c.WriterId, out var writerProfile) ? writerProfile.Name : "??").Yield() }, {"writerID", new object[] {c.WriterId}}, {"templateID", new object[] {c.TemplateId ?? 0}}, {UmbracoContentIndex.VariesByCultureFieldName, new object[] {"n"}}, diff --git a/src/Umbraco.Tests/Services/UserServiceTests.cs b/src/Umbraco.Tests/Services/UserServiceTests.cs index a96385a923..016085c352 100644 --- a/src/Umbraco.Tests/Services/UserServiceTests.cs +++ b/src/Umbraco.Tests/Services/UserServiceTests.cs @@ -924,6 +924,24 @@ namespace Umbraco.Tests.Services Assert.AreEqual(user.Id, profile.Id); } + [Test] + public void Get_By_Profile_Id_Must_return_null_if_user_not_exists() + { + var profile = ServiceContext.UserService.GetProfileById(42); + + // Assert + Assert.IsNull(profile); + } + + [Test] + public void GetProfilesById_Must_empty_if_users_not_exists() + { + var profiles = ServiceContext.UserService.GetProfilesById(42); + + // Assert + CollectionAssert.IsEmpty(profiles); + } + [Test] public void Get_User_By_Username() { diff --git a/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs b/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs index 1653de827d..e9f18d8947 100644 --- a/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs +++ b/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs @@ -30,16 +30,22 @@ namespace Umbraco.Tests.UmbracoExamine /// internal static class IndexInitializer { - public static ContentValueSetBuilder GetContentValueSetBuilder(PropertyEditorCollection propertyEditors, bool publishedValuesOnly) + public static ContentValueSetBuilder GetContentValueSetBuilder(PropertyEditorCollection propertyEditors, IScopeProvider scopeProvider, bool publishedValuesOnly) { - var contentValueSetBuilder = new ContentValueSetBuilder(propertyEditors, new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() }), GetMockUserService(), publishedValuesOnly); + var contentValueSetBuilder = new ContentValueSetBuilder( + propertyEditors, + new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() }), + GetMockUserService(), + scopeProvider, + publishedValuesOnly); + return contentValueSetBuilder; } - public static ContentIndexPopulator GetContentIndexRebuilder(PropertyEditorCollection propertyEditors, IContentService contentService, ISqlContext sqlContext, bool publishedValuesOnly) + public static ContentIndexPopulator GetContentIndexRebuilder(PropertyEditorCollection propertyEditors, IContentService contentService, IScopeProvider scopeProvider, bool publishedValuesOnly) { - var contentValueSetBuilder = GetContentValueSetBuilder(propertyEditors, publishedValuesOnly); - var contentIndexDataSource = new ContentIndexPopulator(true, null, contentService, sqlContext, contentValueSetBuilder); + var contentValueSetBuilder = GetContentValueSetBuilder(propertyEditors, scopeProvider, publishedValuesOnly); + var contentIndexDataSource = new ContentIndexPopulator(true, null, contentService, scopeProvider.SqlContext, contentValueSetBuilder); return contentIndexDataSource; } diff --git a/src/Umbraco.Tests/UmbracoExamine/IndexTest.cs b/src/Umbraco.Tests/UmbracoExamine/IndexTest.cs index 9e59422310..acb26fb8f6 100644 --- a/src/Umbraco.Tests/UmbracoExamine/IndexTest.cs +++ b/src/Umbraco.Tests/UmbracoExamine/IndexTest.cs @@ -29,7 +29,7 @@ namespace Umbraco.Tests.UmbracoExamine [Test] public void Index_Property_Data_With_Value_Indexer() { - var contentValueSetBuilder = IndexInitializer.GetContentValueSetBuilder(Factory.GetInstance(), false); + var contentValueSetBuilder = IndexInitializer.GetContentValueSetBuilder(Factory.GetInstance(), ScopeProvider, false); using (var luceneDir = new RandomIdRamDirectory()) using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir, @@ -121,7 +121,7 @@ namespace Umbraco.Tests.UmbracoExamine [Test] public void Rebuild_Index() { - var contentRebuilder = IndexInitializer.GetContentIndexRebuilder(Factory.GetInstance(), IndexInitializer.GetMockContentService(), ScopeProvider.SqlContext, false); + var contentRebuilder = IndexInitializer.GetContentIndexRebuilder(Factory.GetInstance(), IndexInitializer.GetMockContentService(), ScopeProvider, false); var mediaRebuilder = IndexInitializer.GetMediaIndexRebuilder(Factory.GetInstance(), IndexInitializer.GetMockMediaService()); using (var luceneDir = new RandomIdRamDirectory()) @@ -149,7 +149,7 @@ namespace Umbraco.Tests.UmbracoExamine [Test] public void Index_Protected_Content_Not_Indexed() { - var rebuilder = IndexInitializer.GetContentIndexRebuilder(Factory.GetInstance(), IndexInitializer.GetMockContentService(), ScopeProvider.SqlContext, false); + var rebuilder = IndexInitializer.GetContentIndexRebuilder(Factory.GetInstance(), IndexInitializer.GetMockContentService(), ScopeProvider, false); using (var luceneDir = new RandomIdRamDirectory()) @@ -274,7 +274,7 @@ namespace Umbraco.Tests.UmbracoExamine [Test] public void Index_Reindex_Content() { - var rebuilder = IndexInitializer.GetContentIndexRebuilder(Factory.GetInstance(), IndexInitializer.GetMockContentService(), ScopeProvider.SqlContext, false); + var rebuilder = IndexInitializer.GetContentIndexRebuilder(Factory.GetInstance(), IndexInitializer.GetMockContentService(), ScopeProvider, false); using (var luceneDir = new RandomIdRamDirectory()) using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir, validator: new ContentValueSetValidator(false))) @@ -315,7 +315,7 @@ namespace Umbraco.Tests.UmbracoExamine public void Index_Delete_Index_Item_Ensure_Heirarchy_Removed() { - var rebuilder = IndexInitializer.GetContentIndexRebuilder(Factory.GetInstance(), IndexInitializer.GetMockContentService(), ScopeProvider.SqlContext, false); + var rebuilder = IndexInitializer.GetContentIndexRebuilder(Factory.GetInstance(), IndexInitializer.GetMockContentService(), ScopeProvider, false); using (var luceneDir = new RandomIdRamDirectory()) using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir)) diff --git a/src/Umbraco.Tests/UmbracoExamine/SearchTests.cs b/src/Umbraco.Tests/UmbracoExamine/SearchTests.cs index a45a33ec00..96e8892cd1 100644 --- a/src/Umbraco.Tests/UmbracoExamine/SearchTests.cs +++ b/src/Umbraco.Tests/UmbracoExamine/SearchTests.cs @@ -55,7 +55,7 @@ namespace Umbraco.Tests.UmbracoExamine allRecs); var propertyEditors = Factory.GetInstance(); - var rebuilder = IndexInitializer.GetContentIndexRebuilder(propertyEditors, contentService, ScopeProvider.SqlContext, true); + var rebuilder = IndexInitializer.GetContentIndexRebuilder(propertyEditors, contentService, ScopeProvider, true); using (var luceneDir = new RandomIdRamDirectory()) using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir)) diff --git a/src/Umbraco.Web/Search/ExamineComposer.cs b/src/Umbraco.Web/Search/ExamineComposer.cs index b30f0cbe03..64eeb6978a 100644 --- a/src/Umbraco.Web/Search/ExamineComposer.cs +++ b/src/Umbraco.Web/Search/ExamineComposer.cs @@ -4,6 +4,7 @@ using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Scoping; using Umbraco.Core.Services; using Umbraco.Core.Strings; using Umbraco.Examine; @@ -36,12 +37,14 @@ namespace Umbraco.Web.Search factory.GetInstance(), factory.GetInstance(), factory.GetInstance(), + factory.GetInstance(), true)); composition.RegisterUnique(factory => new ContentValueSetBuilder( factory.GetInstance(), factory.GetInstance(), factory.GetInstance(), + factory.GetInstance(), false)); composition.RegisterUnique, MediaValueSetBuilder>(); composition.RegisterUnique, MemberValueSetBuilder>();