From 84fd5dc2ee52a713f5fe168ab38c1bcab8fbb285 Mon Sep 17 00:00:00 2001 From: Stephan Date: Mon, 4 Sep 2017 19:49:28 +0200 Subject: [PATCH 1/8] perfs - IContentService.IsPublishable --- src/Umbraco.Core/EnumerableExtensions.cs | 11 +++ src/Umbraco.Core/Services/ContentService.cs | 67 +++++++------------ .../Services/ContentServiceTests.cs | 15 +++++ 3 files changed, 50 insertions(+), 43 deletions(-) diff --git a/src/Umbraco.Core/EnumerableExtensions.cs b/src/Umbraco.Core/EnumerableExtensions.cs index e8565f3bc7..bb115b394d 100644 --- a/src/Umbraco.Core/EnumerableExtensions.cs +++ b/src/Umbraco.Core/EnumerableExtensions.cs @@ -295,5 +295,16 @@ namespace Umbraco.Core return list1Groups.Count == list2Groups.Count && list1Groups.All(g => g.Count() == list2Groups[g.Key].Count()); } + + public static IEnumerable SkipLast(this IEnumerable source) + { + using (var e = source.GetEnumerator()) + { + if (e.MoveNext() == false) yield break; + + for (var value = e.Current; e.MoveNext(); value = e.Current) + yield return value; + } + } } } diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 09e6068ce1..913a83c1cd 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -903,15 +903,30 @@ namespace Umbraco.Core.Services /// True if the Content can be published, otherwise False public bool IsPublishable(IContent content) { - //If the passed in content has yet to be saved we "fallback" to checking the Parent - //because if the Parent is publishable then the current content can be Saved and Published - if (content.HasIdentity == false) - { - var parent = GetById(content.ParentId); - return IsPublishable(parent, true); - } + // get ids from path + // skip the first one that has to be -1 - and we don't care + // skip the last one that has to be "this" - and it's ok to stop at the parent + var ids = content.Path.Split(',').Skip(1).SkipLast().Select(int.Parse).ToArray(); + if (ids.Length == 0) + return false; - return IsPublishable(content, false); + // if the first one is recycle bin, fail fast + if (ids[0] == Constants.System.RecycleBinContent) + return false; + + // fixme - move to repository? + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var sql = new Sql(@" + SELECT id + FROM umbracoNode + JOIN cmsDocument ON umbracoNode.id=cmsDocument.nodeId AND cmsDocument.published=@0 + WHERE umbracoNode.trashed=@1 AND umbracoNode.id IN (@2)", + true, false, ids); + Console.WriteLine(sql.SQL); + var x = uow.Database.Fetch(sql); + return ids.Length == x.Count; + } } /// @@ -1062,7 +1077,7 @@ namespace Umbraco.Core.Services descendant.WriterId = userId; descendant.ChangeTrashedState(true, descendant.ParentId); repository.AddOrUpdate(descendant); - + moveInfo.Add(new MoveEventInfo(descendant, descendant.Path, descendant.ParentId)); } @@ -2311,40 +2326,6 @@ namespace Umbraco.Core.Services } } - /// - /// Checks if the passed in can be published based on the anscestors publish state. - /// - /// - /// Check current is only used when falling back to checking the Parent of non-saved content, as - /// non-saved content doesn't have a valid path yet. - /// - /// to check if anscestors are published - /// Boolean indicating whether the passed in content should also be checked for published versions - /// True if the Content can be published, otherwise False - private bool IsPublishable(IContent content, bool checkCurrent) - { - var ids = content.Path.Split(',').Select(int.Parse).ToList(); - foreach (var id in ids) - { - //If Id equals that of the recycle bin we return false because nothing in the bin can be published - if (id == Constants.System.RecycleBinContent) - return false; - - //We don't check the System Root, so just continue - if (id == Constants.System.Root) continue; - - //If the current id equals that of the passed in content and if current shouldn't be checked we skip it. - if (checkCurrent == false && id == content.Id) continue; - - //Check if the content for the current id is published - escape the loop if we encounter content that isn't published - var hasPublishedVersion = HasPublishedVersion(id); - if (hasPublishedVersion == false) - return false; - } - - return true; - } - private PublishStatusType CheckAndLogIsPublishable(IContent content) { //Check if parent is published (although not if its a root node) - if parent isn't published this Content cannot be published diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index 79bf91716e..6c1f388ee5 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -998,6 +998,21 @@ namespace Umbraco.Tests.Services Assert.That(content.Published, Is.True); } + [Test] + public void IsPublishable() + { + // Arrange + var contentService = ServiceContext.ContentService; + var parent = contentService.CreateContent("parent", -1, "umbTextpage"); + contentService.SaveAndPublishWithStatus(parent); + var content = contentService.CreateContent("child", parent, "umbTextpage"); + contentService.Save(content); + + Assert.IsTrue(contentService.IsPublishable(content)); + contentService.UnPublish(parent); + Assert.IsFalse(contentService.IsPublishable(content)); + } + [Test] public void Can_Publish_Content_WithEvents() { From 8d6c8ef282799dfe96082a3b31b205a967194543 Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 6 Sep 2017 14:20:09 +0200 Subject: [PATCH 2/8] perfs - support LocalDb --- src/Umbraco.Core/CoreBootManager.cs | 23 + src/Umbraco.Core/Persistence/LocalDb.cs | 959 ++++++++++++++++++++++++ src/Umbraco.Core/Umbraco.Core.csproj | 1 + 3 files changed, 983 insertions(+) create mode 100644 src/Umbraco.Core/Persistence/LocalDb.cs diff --git a/src/Umbraco.Core/CoreBootManager.cs b/src/Umbraco.Core/CoreBootManager.cs index dcce15f419..a0455d820a 100644 --- a/src/Umbraco.Core/CoreBootManager.cs +++ b/src/Umbraco.Core/CoreBootManager.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Configuration; using System.IO; using System.Linq; +using System.Text.RegularExpressions; using System.Threading; using System.Web; using AutoMapper; @@ -399,6 +400,28 @@ namespace Umbraco.Core if (ApplicationContext.IsConfigured == false) return; if (ApplicationContext.DatabaseContext.IsDatabaseConfigured == false) return; + // deal with localdb + var databaseContext = ApplicationContext.DatabaseContext; + var localdbex = new Regex(@"\(localdb\)\\([a-zA-Z0-9-_]+)(;|$)"); + var m = localdbex.Match(databaseContext.ConnectionString); + if (m.Success) + { + var instanceName = m.Groups[1].Value; + ProfilingLogger.Logger.Info(string.Format("LocalDb instance \"{0}\"", instanceName)); + + var localDb = new LocalDb(); + if (localDb.IsAvailable == false) + throw new UmbracoStartupFailedException("Umbraco cannot start. LocalDb is not available."); + + if (localDb.InstanceExists(m.Groups[1].Value) == false) + { + if (localDb.CreateInstance(instanceName) == false) + throw new UmbracoStartupFailedException(string.Format("Umbraco cannot start. LocalDb cannot create instance \"{0}\".", instanceName)); + if (localDb.StartInstance(instanceName) == false) + throw new UmbracoStartupFailedException(string.Format("Umbraco cannot start. LocalDb cannot start instance \"{0}\".", instanceName)); + } + } + //try now if (ApplicationContext.DatabaseContext.CanConnect) return; diff --git a/src/Umbraco.Core/Persistence/LocalDb.cs b/src/Umbraco.Core/Persistence/LocalDb.cs new file mode 100644 index 0000000000..084122ac3a --- /dev/null +++ b/src/Umbraco.Core/Persistence/LocalDb.cs @@ -0,0 +1,959 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.SqlClient; +using System.Diagnostics; +using System.IO; +using System.Linq; + +namespace Umbraco.Core.Persistence +{ + /// + /// Manages LocalDB databases. + /// + /// + /// Latest version is SQL Server 2016 Express LocalDB, + /// see https://docs.microsoft.com/en-us/sql/database-engine/configure-windows/sql-server-2016-express-localdb + /// which can be installed by downloading the Express installer from https://www.microsoft.com/en-us/sql-server/sql-server-downloads + /// (about 5MB) then select 'download media' to download SqlLocalDB.msi (about 44MB), which you can execute. This installs + /// LocalDB only. Though you probably want to install the full Express. You may also want to install SQL Server Management + /// Studio which can be used to connect to LocalDB databases. + /// See also https://github.com/ritterim/automation-sql which is a somewhat simpler version of this. + /// + public class LocalDb + { + private int _version; + private bool _hasVersion; + + #region Availability & Version + + /// + /// Gets the LocalDb installed version. + /// + /// If more than one version is installed, returns the highest available. Returns + /// the major version as an integer e.g. 11, 12... + /// Thrown when LocalDb is not available. + public int Version + { + get + { + EnsureVersion(); + if (_version <= 0) + throw new InvalidOperationException("LocalDb is not available."); + return _version; + } + } + + /// + /// Ensures that the LocalDb version is detected. + /// + private void EnsureVersion() + { + if (_hasVersion) return; + DetectVersion(); + _hasVersion = true; + } + + /// + /// Gets a value indicating whether LocalDb is available. + /// + public bool IsAvailable + { + get + { + EnsureVersion(); + return _version > 0; + } + } + + /// + /// Ensures that LocalDb is available. + /// + /// Thrown when LocalDb is not available. + private void EnsureAvailable() + { + if (IsAvailable == false) + throw new InvalidOperationException("LocalDb is not available."); + } + + /// + /// Detects LocalDb installed version. + /// + /// If more than one version is installed, the highest available is detected. + private void DetectVersion() + { + _hasVersion = true; + _version = -1; + + var programFiles = Environment.GetEnvironmentVariable("ProgramFiles"); + if (programFiles == null) return; + + // detect 14, 13, 12, 11 + for (var i = 14; i > 10; i--) + { + var path = Path.Combine(programFiles, string.Format(@"Microsoft SQL Server\{0}0\Tools\Binn\SqlLocalDB.exe", i)); + if (File.Exists(path) == false) continue; + _version = i; + break; + } + } + + #endregion + + #region Instances + + /// + /// Gets the name of existing LocalDb instances. + /// + /// The name of existing LocalDb instances. + /// Thrown when LocalDb is not available. + public string[] GetInstances() + { + EnsureAvailable(); + string output, error; + var rc = ExecuteSqlLocalDb("i", out output, out error); // info + if (rc != 0 || error != string.Empty) return null; + return output.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); + } + + /// + /// Gets a value indicating whether a LocalDb instance exists. + /// + /// The name of the instance. + /// A value indicating whether a LocalDb instance with the specified name exists. + /// Thrown when LocalDb is not available. + public bool InstanceExists(string instanceName) + { + EnsureAvailable(); + var instances = GetInstances(); + return instances != null && instances.Contains(instanceName); + } + + /// + /// Creates a LocalDb instance. + /// + /// The name of the instance. + /// A value indicating whether the instance was created without errors. + /// Thrown when LocalDb is not available. + public bool CreateInstance(string instanceName) + { + EnsureAvailable(); + string output, error; + return ExecuteSqlLocalDb(string.Format("c \"{0}\"", instanceName), out output, out error) == 0 && error == string.Empty; + } + + /// + /// Drops a LocalDb instance. + /// + /// The name of the instance. + /// A value indicating whether the instance was dropped without errors. + /// Thrown when LocalDb is not available. + /// + /// When an instance is dropped all the attached database files are deleted. + /// Successful if the instance does not exist. + /// + public bool DropInstance(string instanceName) + { + EnsureAvailable(); + var instance = GetInstance(instanceName); + if (instance == null) return true; + instance.DropDatabases(); // else the files remain + + // -i force NOWAIT, -k kills + string output, error; + return ExecuteSqlLocalDb(string.Format("p \"{0}\" -i", instanceName), out output, out error) == 0 && error == string.Empty + && ExecuteSqlLocalDb(string.Format("d \"{0}\"", instanceName), out output, out error) == 0 && error == string.Empty; + } + + /// + /// Stops a LocalDb instance. + /// + /// The name of the instance. + /// A value indicating whether the instance was stopped without errors. + /// Thrown when LocalDb is not available. + /// + /// Successful if the instance does not exist. + /// + public bool StopInstance(string instanceName) + { + EnsureAvailable(); + if (InstanceExists(instanceName) == false) return true; + + // -i force NOWAIT, -k kills + string output, error; + return ExecuteSqlLocalDb(string.Format("p \"{0}\" -i", instanceName), out output, out error) == 0 && error == string.Empty; + } + + /// + /// Stops a LocalDb instance. + /// + /// The name of the instance. + /// A value indicating whether the instance was started without errors. + /// Thrown when LocalDb is not available. + /// + /// Failed if the instance does not exist. + /// + public bool StartInstance(string instanceName) + { + EnsureAvailable(); + if (InstanceExists(instanceName) == false) return false; + string output, error; + return ExecuteSqlLocalDb(string.Format("s \"{0}\"", instanceName), out output, out error) == 0 && error == string.Empty; + } + + /// + /// Gets a LocalDb instance. + /// + /// The name of the instance. + /// The instance with the specified name if it exists, otherwise null. + /// Thrown when LocalDb is not available. + public Instance GetInstance(string instanceName) + { + EnsureAvailable(); + return InstanceExists(instanceName) ? new Instance(instanceName) : null; + } + + #endregion + + #region Databases + + /// + /// Represents a LocalDb instance. + /// + /// + /// LocalDb is assumed to be available, and the instance is assumed to exist. + /// + public class Instance + { + private readonly string _masterCstr; + + /// + /// Gets the name of the instance. + /// + public string InstanceName { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + /// + public Instance(string instanceName) + { + InstanceName = instanceName; + _masterCstr = string.Format(@"Server=(localdb)\{0};Integrated Security=True;", instanceName); + } + + /// + /// Gets a LocalDb connection string. + /// + /// The name of the database. + /// The connection string for the specified database. + /// + /// The database should exist in the LocalDb instance. + /// + public string GetConnectionString(string databaseName) + { + return _masterCstr + string.Format(@"Database={0};", databaseName); + } + + /// + /// Gets a LocalDb connection string for an attached database. + /// + /// The name of the database. + /// The directory containing database files. + /// The connection string for the specified database. + /// + /// The database should not exist in the LocalDb instance. + /// It will be attached with its name being its MDF filename (full path), uppercased, when + /// the first connection is opened, and remain attached until explicitely detached. + /// + public string GetAttachedConnectionString(string databaseName, string filesPath) + { + string logName, baseFilename, baseLogFilename, mdfFilename, ldfFilename; + GetDatabaseFiles(databaseName, filesPath, out logName, out baseFilename, out baseLogFilename, out mdfFilename, out ldfFilename); + + return _masterCstr + string.Format(@"AttachDbFileName='{0}';", mdfFilename); + } + + /// + /// Gets the name of existing databases. + /// + /// The name of existing databases. + public string[] GetDatabases() + { + var userDatabases = new List(); + + using (var conn = new SqlConnection(_masterCstr)) + using (var cmd = conn.CreateCommand()) + { + conn.Open(); + + var databases = new Dictionary(); + + SetCommand(cmd, @" + SELECT name, filename FROM sys.sysdatabases"); + + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + databases[reader.GetString(0)] = reader.GetString(1); + } + } + + foreach (var database in databases) + { + var dbname = database.Key; + + if (dbname == "master" || dbname == "tempdb" || dbname == "model" || dbname == "msdb") + continue; + + // fixme - shall we deal with stale databases? + // fixme - is it always ok to assume file names? + //var mdf = database.Value; + //var ldf = mdf.Replace(".mdf", "_log.ldf"); + //if (staleOnly && File.Exists(mdf) && File.Exists(ldf)) + // continue; + + //ExecuteDropDatabase(cmd, dbname, mdf, ldf); + //count++; + + userDatabases.Add(dbname); + } + } + + return userDatabases.ToArray(); + } + + /// + /// Gets a value indicating whether a database exists. + /// + /// The name of the database. + /// A value indicating whether a database with the specified name exists. + /// + /// A database exists if it is registered in the instance, and its files exist. If the database + /// is registered but some of its files are missing, the database is dropped. + /// + public bool DatabaseExists(string databaseName) + { + using (var conn = new SqlConnection(_masterCstr)) + using (var cmd = conn.CreateCommand()) + { + conn.Open(); + + var mdf = GetDatabase(cmd, databaseName); + if (mdf == null) return false; + + // it can exist, even though its files have been deleted + // if files exist assume all is ok (should we try to connect?) + var ldf = GetLogFilename(mdf); + if (File.Exists(mdf) && File.Exists(ldf)) + return true; + + ExecuteDropDatabase(cmd, databaseName, mdf, ldf); + } + + return false; + } + + /// + /// Creates a new database. + /// + /// The name of the database. + /// The directory containing database files. + /// A value indicating whether the database was created without errors. + /// + /// Failed if a database with the specified name already exists in the instance, + /// or if the database files already exist in the specified directory. + /// + public bool CreateDatabase(string databaseName, string filesPath) + { + string logName, baseFilename, baseLogFilename, mdfFilename, ldfFilename; + GetDatabaseFiles(databaseName, filesPath, out logName, out baseFilename, out baseLogFilename, out mdfFilename, out ldfFilename); + + using (var conn = new SqlConnection(_masterCstr)) + using (var cmd = conn.CreateCommand()) + { + conn.Open(); + + var mdf = GetDatabase(cmd, databaseName); + if (mdf != null) return false; + + // cannot use parameters on CREATE DATABASE + // ie "CREATE DATABASE @0 ..." does not work + SetCommand(cmd, string.Format(@" + CREATE DATABASE {0} + ON (NAME=N{1}, FILENAME={2}) + LOG ON (NAME=N{3}, FILENAME={4})", + QuotedName(databaseName), + QuotedName(databaseName, '\''), QuotedName(mdfFilename, '\''), + QuotedName(logName, '\''), QuotedName(ldfFilename, '\''))); + + var unused = cmd.ExecuteNonQuery(); + } + return true; + } + + /// + /// Drops a database. + /// + /// The name of the database. + /// A value indicating whether the database was dropped without errors. + /// + /// Successful if the database does not exist. + /// Deletes the database files. + /// + public bool DropDatabase(string databaseName) + { + using (var conn = new SqlConnection(_masterCstr)) + using (var cmd = conn.CreateCommand()) + { + conn.Open(); + + SetCommand(cmd, @" + SELECT name, filename FROM master.dbo.sysdatabases WHERE ('[' + name + ']' = @0 OR name = @0)", + databaseName); + + var mdf = GetDatabase(cmd, databaseName); + if (mdf == null) return true; + + ExecuteDropDatabase(cmd, databaseName, mdf); + } + + return true; + } + + /// + /// Drops stale databases. + /// + /// The number of databases that were dropped. + /// + /// A database is considered stale when its files cannot be found. + /// + public int DropStaleDatabases() + { + return DropDatabases(true); + } + + /// + /// Drops databases. + /// + /// A value indicating whether to delete only stale database. + /// The number of databases that were dropped. + /// + /// A database is considered stale when its files cannot be found. + /// + public int DropDatabases(bool staleOnly = false) + { + var count = 0; + using (var conn = new SqlConnection(_masterCstr)) + using (var cmd = conn.CreateCommand()) + { + conn.Open(); + + var databases = new Dictionary(); + + SetCommand(cmd, @" + SELECT name, filename FROM sys.sysdatabases"); + + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + databases[reader.GetString(0)] = reader.GetString(1); + } + } + + foreach (var database in databases) + { + var dbname = database.Key; + + if (dbname == "master" || dbname == "tempdb" || dbname == "model" || dbname == "msdb") + continue; + + var mdf = database.Value; + var ldf = mdf.Replace(".mdf", "_log.ldf"); + if (staleOnly && File.Exists(mdf) && File.Exists(ldf)) + continue; + + ExecuteDropDatabase(cmd, dbname, mdf, ldf); + count++; + } + } + + return count; + } + + /// + /// Detaches a database. + /// + /// The name of the database. + /// The directory containing the database files. + /// Thrown when a database with the specified name does not exist. + public string DetachDatabase(string databaseName) + { + using (var conn = new SqlConnection(_masterCstr)) + using (var cmd = conn.CreateCommand()) + { + conn.Open(); + + var mdf = GetDatabase(cmd, databaseName); + if (mdf == null) + throw new InvalidOperationException("Database does not exist."); + + DetachDatabase(cmd, databaseName); + + return Path.GetDirectoryName(mdf); + } + } + + /// + /// Attaches a database. + /// + /// The name of the database. + /// The directory containing database files. + /// Thrown when a database with the specified name already exists. + public void AttachDatabase(string databaseName, string filesPath) + { + using (var conn = new SqlConnection(_masterCstr)) + using (var cmd = conn.CreateCommand()) + { + conn.Open(); + + var mdf = GetDatabase(cmd, databaseName); + if (mdf != null) + throw new InvalidOperationException("Database already exists."); + + AttachDatabase(cmd, databaseName, filesPath); + } + } + + /// + /// Gets the file names of a database. + /// + /// The name of the database. + /// The MDF logical name. + /// The LDF logical name. + /// The MDF filename. + /// The LDF filename. + public void GetFilenames(string databaseName, + out string mdfName, out string ldfName, + out string mdfFilename, out string ldfFilename) + { + using (var conn = new SqlConnection(_masterCstr)) + using (var cmd = conn.CreateCommand()) + { + conn.Open(); + + GetFilenames(cmd, databaseName, out mdfName, out ldfName, out mdfFilename, out ldfFilename); + } + } + + /// + /// Kills all existing connections. + /// + /// The name of the database. + public void KillConnections(string databaseName) + { + using (var conn = new SqlConnection(_masterCstr)) + using (var cmd = conn.CreateCommand()) + { + conn.Open(); + + SetCommand(cmd, @" + DECLARE @sql VARCHAR(MAX); + SELECT @sql = COALESCE(@sql,'') + 'kill ' + CONVERT(VARCHAR, SPId) + ';' + FROM master.sys.sysprocesses + WHERE DBId = DB_ID(@0) AND SPId <> @@SPId; + EXEC(@sql);", + databaseName); + cmd.ExecuteNonQuery(); + } + } + + /// + /// Gets a database. + /// + /// The Sql Command. + /// The name of the database. + /// The full filename of the MDF file, if the database exists, otherwise null. + private static string GetDatabase(SqlCommand cmd, string databaseName) + { + SetCommand(cmd, @" + SELECT name, filename FROM master.dbo.sysdatabases WHERE ('[' + name + ']' = @0 OR name = @0)", + databaseName); + + string mdf = null; + using (var reader = cmd.ExecuteReader()) + { + if (reader.Read()) + mdf = reader.GetString(1) ?? string.Empty; + while (reader.Read()) + { + } + } + + return mdf; + } + + /// + /// Drops a database and its files. + /// + /// The Sql command. + /// The name of the database. + /// The name of the database (MDF) file. + /// The name of the log (LDF) file. + private static void ExecuteDropDatabase(SqlCommand cmd, string databaseName, string mdf, string ldf = null) + { + try + { + // cannot use parameters on ALTER DATABASE + // ie "ALTER DATABASE @0 ..." does not work + SetCommand(cmd, string.Format(@" + ALTER DATABASE {0} SET SINGLE_USER WITH ROLLBACK IMMEDIATE", + QuotedName(databaseName))); + + var unused1 = cmd.ExecuteNonQuery(); + } + catch (SqlException e) + { + if (e.Message.Contains("Unable to open the physical file") && e.Message.Contains("Operating system error 2:")) + { + // quite probably, the files were missing + // yet, it should be possible to drop the database anyways + // but we'll have to deal with the files + } + else + { + // no idea, throw + throw; + } + } + + // cannot use parameters on DROP DATABASE + // ie "DROP DATABASE @0 ..." does not work + SetCommand(cmd, string.Format(@" + DROP DATABASE {0}", + QuotedName(databaseName))); + + var unused2 = cmd.ExecuteNonQuery(); + + // be absolutely sure + if (File.Exists(mdf)) File.Delete(mdf); + ldf = ldf ?? GetLogFilename(mdf); + if (File.Exists(ldf)) File.Delete(ldf); + } + + /// + /// Gets the log (LDF) filename corresponding to a database (MDF) filename. + /// + /// The MDF filename. + /// + private static string GetLogFilename(string mdfFilename) + { + if (mdfFilename.EndsWith(".mdf") == false) + throw new ArgumentException("Not a valid MDF filename (no .mdf extension).", "mdfFilename"); + return mdfFilename.Substring(0, mdfFilename.Length - ".mdf".Length) + "_log.ldf"; + } + + /// + /// Detaches a database. + /// + /// The Sql command. + /// The name of the database. + private static void DetachDatabase(SqlCommand cmd, string databaseName) + { + // cannot use parameters on ALTER DATABASE + // ie "ALTER DATABASE @0 ..." does not work + SetCommand(cmd, string.Format(@" + ALTER DATABASE {0} SET SINGLE_USER WITH ROLLBACK IMMEDIATE", + QuotedName(databaseName))); + + var unused1 = cmd.ExecuteNonQuery(); + + SetCommand(cmd, @" + EXEC sp_detach_db @dbname=@0", + databaseName); + + var unused2 = cmd.ExecuteNonQuery(); + } + + /// + /// Attaches a database. + /// + /// The Sql command. + /// The name of the database. + /// The directory containing database files. + private static void AttachDatabase(SqlCommand cmd, string databaseName, string filesPath) + { + string logName, baseFilename, baseLogFilename, mdfFilename, ldfFilename; + GetDatabaseFiles(databaseName, filesPath, out logName, out baseFilename, out baseLogFilename, out mdfFilename, out ldfFilename); + + // cannot use parameters on CREATE DATABASE + // ie "CREATE DATABASE @0 ..." does not work + SetCommand(cmd, string.Format(@" + CREATE DATABASE {0} + ON (NAME=N{1}, FILENAME={2}) + LOG ON (NAME=N{3}, FILENAME={4}) + FOR ATTACH", + QuotedName(databaseName), + QuotedName(databaseName, '\''), QuotedName(mdfFilename, '\''), + QuotedName(logName, '\''), QuotedName(ldfFilename, '\''))); + + var unused = cmd.ExecuteNonQuery(); + } + + /// + /// Sets a database command. + /// + /// The command. + /// The command text. + /// The command arguments. + /// + /// The command text must refer to arguments as @0, @1... each referring + /// to the corresponding position in . + /// + private static void SetCommand(SqlCommand cmd, string sql, params object[] args) + { + cmd.CommandType = CommandType.Text; + cmd.CommandText = sql; + cmd.Parameters.Clear(); + for (var i = 0; i < args.Length; i++) + cmd.Parameters.AddWithValue("@" + i, args[i]); + } + + /// + /// Gets the file names of a database. + /// + /// The Sql command. + /// The name of the database. + /// The MDF logical name. + /// The LDF logical name. + /// The MDF filename. + /// The LDF filename. + private void GetFilenames(SqlCommand cmd, string databaseName, + out string mdfName, out string ldfName, + out string mdfFilename, out string ldfFilename) + { + mdfName = ldfName = mdfFilename = ldfFilename = null; + + SetCommand(cmd, @" + SELECT DB_NAME(database_id), type_desc, name, physical_name + FROM master.sys.master_files + WHERE database_id=DB_ID(@0)", + databaseName); + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + var type = reader.GetString(1); + if (type == "ROWS") + { + mdfName = reader.GetString(2); + ldfName = reader.GetString(3); + } + else if (type == "LOG") + { + ldfName = reader.GetString(2); + ldfFilename = reader.GetString(3); + } + } + } + } + } + + /// + /// Copy database files. + /// + /// The name of the source database. + /// The directory containing source database files. + /// The name of the target database. + /// The directory containing target database files. + /// The source database files extension. + /// The target database files extension. + /// A value indicating whether to overwrite the target files. + /// A value indicating whether to delete the source files. + /// + /// The , , + /// and parameters are optional. If they result in target being identical + /// to source, no copy is performed. If is false, nothing happens, otherwise the source + /// files are deleted. + /// If target is not identical to source, files are copied or moved, depending on the value of . + /// Extensions are used eg to copy MyDatabase.mdf to MyDatabase.mdf.temp. + /// + public void CopyDatabaseFiles(string databaseName, string filesPath, + string targetDatabaseName = null, string targetFilesPath = null, + string sourceExtension = null, string targetExtension = null, + bool overwrite = false, bool delete = false) + { + var nop = (targetFilesPath == null || targetFilesPath == filesPath) + && (targetDatabaseName == null || targetDatabaseName == databaseName) + && (sourceExtension == null && targetExtension == null || sourceExtension == targetExtension); + if (nop && delete == false) return; + + string logName, baseFilename, baseLogFilename, mdfFilename, ldfFilename; + GetDatabaseFiles(databaseName, filesPath, + out logName, out baseFilename, out baseLogFilename, out mdfFilename, out ldfFilename); + + if (sourceExtension != null) + { + mdfFilename += "." + sourceExtension; + ldfFilename += "." + sourceExtension; + } + + if (nop) + { + // delete + if (File.Exists(mdfFilename)) File.Delete(mdfFilename); + if (File.Exists(ldfFilename)) File.Delete(ldfFilename); + } + else + { + // copy or copy+delete ie move + string targetLogName, targetBaseFilename, targetLogFilename, targetMdfFilename, targetLdfFilename; + GetDatabaseFiles(targetDatabaseName ?? databaseName, targetFilesPath ?? filesPath, + out targetLogName, out targetBaseFilename, out targetLogFilename, out targetMdfFilename, out targetLdfFilename); + + if (targetExtension != null) + { + targetMdfFilename += "." + targetExtension; + targetLdfFilename += "." + targetExtension; + } + + if (delete) + { + if (overwrite && File.Exists(targetMdfFilename)) File.Delete(targetMdfFilename); + if (overwrite && File.Exists(targetLdfFilename)) File.Delete(targetLdfFilename); + File.Move(mdfFilename, targetMdfFilename); + File.Move(ldfFilename, targetLdfFilename); + } + else + { + File.Copy(mdfFilename, targetMdfFilename, overwrite); + File.Copy(ldfFilename, targetLdfFilename, overwrite); + } + } + } + + /// + /// Gets a value indicating whether database files exist. + /// + /// The name of the source database. + /// The directory containing source database files. + /// The database files extension. + /// A value indicating whether the database files exist. + /// + /// Extensions are used eg to copy MyDatabase.mdf to MyDatabase.mdf.temp. + /// + public bool DatabaseFilesExist(string databaseName, string filesPath, string extension = null) + { + string logName, baseFilename, baseLogFilename, mdfFilename, ldfFilename; + GetDatabaseFiles(databaseName, filesPath, + out logName, out baseFilename, out baseLogFilename, out mdfFilename, out ldfFilename); + + if (extension != null) + { + mdfFilename += "." + extension; + ldfFilename += "." + extension; + } + + return File.Exists(mdfFilename) && File.Exists(ldfFilename); + } + + /// + /// Gets the name of the database files. + /// + /// The name of the database. + /// The directory containing database files. + /// The name of the log. + /// The base filename (the MDF filename without the .mdf extension). + /// The base log filename (the LDF filename without the .ldf extension). + /// The MDF filename. + /// The LDF filename. + private static void GetDatabaseFiles(string databaseName, string filesPath, + out string logName, + out string baseFilename, out string baseLogFilename, + out string mdfFilename, out string ldfFilename) + { + logName = databaseName + "_log"; + baseFilename = Path.Combine(filesPath, databaseName); + baseLogFilename = Path.Combine(filesPath, logName); + mdfFilename = baseFilename + ".mdf"; + ldfFilename = baseFilename + "_log.ldf"; + } + + #endregion + + #region SqlLocalDB + + /// + /// Executes the SqlLocalDB command. + /// + /// The arguments. + /// The command standard output. + /// The command error output. + /// The process exit code. + /// + /// Execution is successful if the exit code is zero, and error is empty. + /// + private int ExecuteSqlLocalDb(string args, out string output, out string error) + { + var programFiles = Environment.GetEnvironmentVariable("ProgramFiles"); + if (programFiles == null) + { + output = string.Empty; + error = "SqlLocalDB.exe not found"; + return -1; + } + + var path = Path.Combine(programFiles, string.Format(@"Microsoft SQL Server\{0}0\Tools\Binn\SqlLocalDB.exe", _version)); + + var p = new Process + { + StartInfo = + { + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + FileName = path, + Arguments = args, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden + } + }; + p.Start(); + output = p.StandardOutput.ReadToEnd(); + error = p.StandardError.ReadToEnd(); + p.WaitForExit(); + + return p.ExitCode; + } + + /// + /// Returns a Unicode string with the delimiters added to make the input string a valid SQL Server delimited identifier. + /// + /// The name to quote. + /// A quote character. + /// + /// + /// This is a C# implementation of T-SQL QUOTEDNAME. + /// is optional, it can be '[' (default), ']', '\'' or '"'. + /// + private static string QuotedName(string name, char quote = '[') + { + switch (quote) + { + case '[': + case ']': + return "[" + name.Replace("]", "]]") + "]"; + case '\'': + return "'" + name.Replace("'", "''") + "'"; + case '"': + return "\"" + name.Replace("\"", "\"\"") + "\""; + default: + throw new NotSupportedException("Not a valid quote character."); + } + } + + #endregion + } +} diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index ec6bfebd5b..8854c3c4ab 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -496,6 +496,7 @@ + From dbda6689b68bb429e8463e7a35b5d1811e37fc0a Mon Sep 17 00:00:00 2001 From: Stephan Date: Tue, 5 Sep 2017 14:51:56 +0200 Subject: [PATCH 3/8] perfs - suspendable --- src/Umbraco.Core/Umbraco.Core.csproj | 3 + .../Cache/CacheRefresherEventHandler.cs | 2 + src/Umbraco.Web/Cache/PageCacheRefresher.cs | 41 +++---- .../Scheduling/ScheduledPublishing.cs | 3 + src/Umbraco.Web/Search/ExamineEvents.cs | 88 +++++++------ src/Umbraco.Web/Suspendable.cs | 116 ++++++++++++++++++ src/Umbraco.Web/Umbraco.Web.csproj | 1 + .../umbraco.presentation/content.cs | 1 + 8 files changed, 196 insertions(+), 59 deletions(-) create mode 100644 src/Umbraco.Web/Suspendable.cs diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 8854c3c4ab..77e84da945 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -134,6 +134,9 @@ + + ..\Umbraco.Web.UI\Bin\umbraco.dll + diff --git a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs index beab3e6cf0..6fcdf2fcf3 100644 --- a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs +++ b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs @@ -823,6 +823,8 @@ namespace Umbraco.Web.Cache var handler = FindHandler(e); if (handler == null) continue; + ApplicationContext.Current.ProfilingLogger.Logger.Info("Handling " + e.Sender + " " + e.EventName); + handler.Invoke(null, new[] { e.Sender, e.Args }); } } diff --git a/src/Umbraco.Web/Cache/PageCacheRefresher.cs b/src/Umbraco.Web/Cache/PageCacheRefresher.cs index e884c9b3b8..d9bb7e9b88 100644 --- a/src/Umbraco.Web/Cache/PageCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/PageCacheRefresher.cs @@ -50,8 +50,9 @@ namespace Umbraco.Web.Cache /// public override void RefreshAll() { - content.Instance.RefreshContentFromDatabase(); - XmlPublishedContent.ClearRequest(); + if (Suspendable.PageCacheRefresher.CanRefreshDocumentCacheFromDatabase) + content.Instance.RefreshContentFromDatabase(); + ClearCaches(); base.RefreshAll(); } @@ -61,11 +62,9 @@ namespace Umbraco.Web.Cache /// The id. public override void Refresh(int id) { - ApplicationContext.Current.ApplicationCache.ClearPartialViewCache(); - content.Instance.UpdateDocumentCache(id); - XmlPublishedContent.ClearRequest(); - DistributedCache.Instance.ClearAllMacroCacheOnCurrentServer(); - DistributedCache.Instance.ClearXsltCacheOnCurrentServer(); + if (Suspendable.PageCacheRefresher.CanUpdateDocumentCache) + content.Instance.UpdateDocumentCache(id); + ClearCaches(); base.Refresh(id); } @@ -75,35 +74,35 @@ namespace Umbraco.Web.Cache /// The id. public override void Remove(int id) { - ApplicationContext.Current.ApplicationCache.ClearPartialViewCache(); - content.Instance.ClearDocumentCache(id, false); - XmlPublishedContent.ClearRequest(); - DistributedCache.Instance.ClearAllMacroCacheOnCurrentServer(); - DistributedCache.Instance.ClearXsltCacheOnCurrentServer(); - ClearAllIsolatedCacheByEntityType(); + if (Suspendable.PageCacheRefresher.CanUpdateDocumentCache) + content.Instance.ClearDocumentCache(id, false); + ClearCaches(); base.Remove(id); } public override void Refresh(IContent instance) { - ApplicationContext.Current.ApplicationCache.ClearPartialViewCache(); - content.Instance.UpdateDocumentCache(new Document(instance)); - XmlPublishedContent.ClearRequest(); - DistributedCache.Instance.ClearAllMacroCacheOnCurrentServer(); - DistributedCache.Instance.ClearXsltCacheOnCurrentServer(); - ClearAllIsolatedCacheByEntityType(); + if (Suspendable.PageCacheRefresher.CanUpdateDocumentCache) + content.Instance.UpdateDocumentCache(new Document(instance)); + ClearCaches(); base.Refresh(instance); } public override void Remove(IContent instance) + { + if (Suspendable.PageCacheRefresher.CanUpdateDocumentCache) + content.Instance.ClearDocumentCache(new Document(instance), false); + ClearCaches(); + base.Remove(instance); + } + + private void ClearCaches() { ApplicationContext.Current.ApplicationCache.ClearPartialViewCache(); - content.Instance.ClearDocumentCache(new Document(instance), false); XmlPublishedContent.ClearRequest(); DistributedCache.Instance.ClearAllMacroCacheOnCurrentServer(); DistributedCache.Instance.ClearXsltCacheOnCurrentServer(); ClearAllIsolatedCacheByEntityType(); - base.Remove(instance); } } } diff --git a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs index 24359d2b50..96cf724d02 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs @@ -27,6 +27,9 @@ namespace Umbraco.Web.Scheduling { if (_appContext == null) return true; // repeat... + if (Suspendable.ScheduledPublishing.CanRun == false) + return true; // repeat, later + switch (_appContext.GetCurrentServerRole()) { case ServerRole.Slave: diff --git a/src/Umbraco.Web/Search/ExamineEvents.cs b/src/Umbraco.Web/Search/ExamineEvents.cs index 7fbbf29b89..e018bac214 100644 --- a/src/Umbraco.Web/Search/ExamineEvents.cs +++ b/src/Umbraco.Web/Search/ExamineEvents.cs @@ -24,7 +24,6 @@ namespace Umbraco.Web.Search /// public sealed class ExamineEvents : ApplicationEventHandler { - /// /// Once the application has started we should bind to all events and initialize the providers. /// @@ -32,9 +31,9 @@ namespace Umbraco.Web.Search /// /// /// We need to do this on the Started event as to guarantee that all resolvers are setup properly. - /// + /// protected override void ApplicationStarted(UmbracoApplicationBase httpApplication, ApplicationContext applicationContext) - { + { LogHelper.Info("Initializing Examine and binding to business logic events"); var registeredProviders = ExamineManager.Instance.IndexProviderCollection @@ -46,14 +45,14 @@ namespace Umbraco.Web.Search if (registeredProviders == 0) return; - //Bind to distributed cache events - this ensures that this logic occurs on ALL servers that are taking part + //Bind to distributed cache events - this ensures that this logic occurs on ALL servers that are taking part // in a load balanced environment. CacheRefresherBase.CacheUpdated += UnpublishedPageCacheRefresherCacheUpdated; CacheRefresherBase.CacheUpdated += PublishedPageCacheRefresherCacheUpdated; CacheRefresherBase.CacheUpdated += MediaCacheRefresherCacheUpdated; CacheRefresherBase.CacheUpdated += MemberCacheRefresherCacheUpdated; CacheRefresherBase.CacheUpdated += ContentTypeCacheRefresherCacheUpdated; - + var contentIndexer = ExamineManager.Instance.IndexProviderCollection[Constants.Examine.InternalIndexer] as UmbracoContentIndexer; if (contentIndexer != null) { @@ -77,6 +76,9 @@ namespace Umbraco.Web.Search /// static void ContentTypeCacheRefresherCacheUpdated(ContentTypeCacheRefresher sender, CacheRefresherEventArgs e) { + if (Suspendable.ExamineEvents.CanIndex == false) + return; + var indexersToUpdated = ExamineManager.Instance.IndexProviderCollection.OfType(); foreach (var provider in indexersToUpdated) { @@ -114,7 +116,7 @@ namespace Umbraco.Web.Search } } - //TODO: We need to update Examine to support re-indexing multiple items at once instead of one by one which will speed up + //TODO: We need to update Examine to support re-indexing multiple items at once instead of one by one which will speed up // the re-indexing process, we don't want to revert to rebuilding the whole thing! if (contentTypesChanged.Count > 0) @@ -129,8 +131,8 @@ namespace Umbraco.Web.Search { ReIndexForContent(contentItem, contentItem.HasPublishedVersion && contentItem.Trashed == false); } - } - } + } + } } if (mediaTypesChanged.Count > 0) { @@ -163,11 +165,14 @@ namespace Umbraco.Web.Search } } } - + } static void MemberCacheRefresherCacheUpdated(MemberCacheRefresher sender, CacheRefresherEventArgs e) { + if (Suspendable.ExamineEvents.CanIndex == false) + return; + switch (e.MessageType) { case MessageType.RefreshById: @@ -215,6 +220,9 @@ namespace Umbraco.Web.Search /// static void MediaCacheRefresherCacheUpdated(MediaCacheRefresher sender, CacheRefresherEventArgs e) { + if (Suspendable.ExamineEvents.CanIndex == false) + return; + switch (e.MessageType) { case MessageType.RefreshById: @@ -252,13 +260,13 @@ namespace Umbraco.Web.Search if (media1 != null) { ReIndexForMedia(media1, media1.Trashed == false); - } + } break; case MediaCacheRefresher.OperationType.Trashed: - + //keep if trashed for indexes supporting unpublished //(delete the index from all indexes not supporting unpublished content) - + DeleteIndexForEntity(payload.Id, true); //We then need to re-index this item for all indexes supporting unpublished content @@ -272,20 +280,20 @@ namespace Umbraco.Web.Search case MediaCacheRefresher.OperationType.Deleted: //permanently remove from all indexes - + DeleteIndexForEntity(payload.Id, false); break; default: throw new ArgumentOutOfRangeException(); - } - } + } + } } break; - case MessageType.RefreshByInstance: - case MessageType.RemoveByInstance: - case MessageType.RefreshAll: + case MessageType.RefreshByInstance: + case MessageType.RemoveByInstance: + case MessageType.RefreshAll: default: //We don't support these, these message types will not fire for media break; @@ -302,6 +310,9 @@ namespace Umbraco.Web.Search /// static void PublishedPageCacheRefresherCacheUpdated(PageCacheRefresher sender, CacheRefresherEventArgs e) { + if (Suspendable.ExamineEvents.CanIndex == false) + return; + switch (e.MessageType) { case MessageType.RefreshById: @@ -312,8 +323,8 @@ namespace Umbraco.Web.Search } break; case MessageType.RemoveById: - - //This is triggered when the item has been unpublished or trashed (which also performs an unpublish). + + //This is triggered when the item has been unpublished or trashed (which also performs an unpublish). var c2 = ApplicationContext.Current.Services.ContentService.GetById((int)e.MessageObject); if (c2 != null) @@ -368,6 +379,9 @@ namespace Umbraco.Web.Search /// static void UnpublishedPageCacheRefresherCacheUpdated(UnpublishedPageCacheRefresher sender, CacheRefresherEventArgs e) { + if (Suspendable.ExamineEvents.CanIndex == false) + return; + switch (e.MessageType) { case MessageType.RefreshById: @@ -378,9 +392,9 @@ namespace Umbraco.Web.Search } break; case MessageType.RemoveById: - + // This is triggered when the item is permanently deleted - + DeleteIndexForEntity((int)e.MessageObject, false); break; case MessageType.RefreshByInstance: @@ -399,7 +413,7 @@ namespace Umbraco.Web.Search { DeleteIndexForEntity(c4.Id, false); } - break; + break; case MessageType.RefreshByJson: var jsonPayloads = UnpublishedPageCacheRefresher.DeserializeFromJsonPayload((string)e.MessageObject); @@ -409,29 +423,28 @@ namespace Umbraco.Web.Search { switch (payload.Operation) { - case UnpublishedPageCacheRefresher.OperationType.Deleted: + case UnpublishedPageCacheRefresher.OperationType.Deleted: //permanently remove from all indexes - + DeleteIndexForEntity(payload.Id, false); break; default: throw new ArgumentOutOfRangeException(); - } - } + } + } } break; - case MessageType.RefreshAll: + case MessageType.RefreshAll: default: //We don't support these, these message types will not fire for unpublished content break; } } - private static void ReIndexForMember(IMember member) { ExamineManager.Instance.ReIndexNode( @@ -447,7 +460,7 @@ namespace Umbraco.Web.Search /// /// /// - + private static void IndexerDocumentWriting(object sender, DocumentWritingEventArgs e) { if (e.Fields.Keys.Contains("nodeName")) @@ -463,7 +476,7 @@ namespace Umbraco.Web.Search )); } } - + private static void ReIndexForMedia(IMedia sender, bool isMediaPublished) { var xml = sender.ToXml(); @@ -497,7 +510,7 @@ namespace Umbraco.Web.Search //if keepIfUnpublished == true then only delete this item from indexes not supporting unpublished content, // otherwise if keepIfUnpublished == false then remove from all indexes - + .Where(x => keepIfUnpublished == false || x.SupportUnpublishedContent == false) .Where(x => x.EnableDefaultEventHandler)); } @@ -518,7 +531,7 @@ namespace Umbraco.Web.Search ExamineManager.Instance.ReIndexNode( xml, IndexTypes.Content, ExamineManager.Instance.IndexProviderCollection.OfType() - + //Index this item for all indexers if the content is published, otherwise if the item is not published // then only index this for indexers supporting unpublished content @@ -531,10 +544,10 @@ namespace Umbraco.Web.Search /// /// /// true if data is going to be returned from cache - /// + /// [Obsolete("This method is no longer used and will be removed from the core in future versions, the cacheOnly parameter has no effect. Use the other ToXDocument overload instead")] public static XDocument ToXDocument(Content node, bool cacheOnly) - { + { return ToXDocument(node); } @@ -542,7 +555,7 @@ namespace Umbraco.Web.Search /// Converts a content node to Xml /// /// - /// + /// private static XDocument ToXDocument(Content node) { if (TypeHelper.IsTypeAssignableFrom(node)) @@ -561,7 +574,7 @@ namespace Umbraco.Web.Search if (xNode.Attributes["nodeTypeAlias"] == null) { - //we'll add the nodeTypeAlias ourselves + //we'll add the nodeTypeAlias ourselves XmlAttribute d = xDoc.CreateAttribute("nodeTypeAlias"); d.Value = node.ContentType.Alias; xNode.Attributes.Append(d); @@ -569,6 +582,5 @@ namespace Umbraco.Web.Search return new XDocument(ExamineXmlExtensions.ToXElement(xNode)); } - } } \ No newline at end of file diff --git a/src/Umbraco.Web/Suspendable.cs b/src/Umbraco.Web/Suspendable.cs new file mode 100644 index 0000000000..29e8c3a540 --- /dev/null +++ b/src/Umbraco.Web/Suspendable.cs @@ -0,0 +1,116 @@ +using System; +using System.Diagnostics; +using Examine; +using Examine.Providers; +using Umbraco.Core; +using Umbraco.Web.Cache; + +namespace Umbraco.Web +{ + public static class Suspendable + { + public static class PageCacheRefresher + { + private static bool _tried, _suspended; + + public static bool CanRefreshDocumentCacheFromDatabase + { + get + { + // trying a full refresh + if (_suspended == false) return true; + _tried = true; // remember we tried + return false; + } + } + + public static bool CanUpdateDocumentCache + { + get + { + // trying a partial update + // ok if not suspended, or if we haven't done a full already + return _suspended == false || _tried == false; + } + } + + public static void SuspendDocumentCache() + { + ApplicationContext.Current.ProfilingLogger.Logger.Info(typeof (PageCacheRefresher), "Suspend document cache."); + _suspended = true; + } + + public static void ResumeDocumentCache() + { + _suspended = false; + + ApplicationContext.Current.ProfilingLogger.Logger.Info(typeof (PageCacheRefresher), string.Format("Resume document cache (reload:{0}).", _tried ? "true" : "false")); + + if (_tried == false) return; + _tried = false; + + var pageRefresher = CacheRefreshersResolver.Current.GetById(new Guid(DistributedCache.PageCacheRefresherId)); + pageRefresher.RefreshAll(); + } + } + + public static class ExamineEvents + { + private static bool _tried, _suspended; + + public static bool CanIndex + { + get + { + if (_suspended == false) return true; + _tried = true; // remember we tried + return false; + } + } + + public static void SuspendIndexers() + { + ApplicationContext.Current.ProfilingLogger.Logger.Info(typeof (ExamineEvents), "Suspend indexers."); + _suspended = true; + } + + public static void ResumeIndexers() + { + _suspended = false; + + ApplicationContext.Current.ProfilingLogger.Logger.Info(typeof (ExamineEvents), string.Format("Resume indexers (rebuild:{0}).", _tried ? "true" : "false")); + + if (_tried == false) return; + _tried = false; + + // fixme - could we fork this on a background thread? + foreach (BaseIndexProvider indexer in ExamineManager.Instance.IndexProviderCollection) + { + indexer.RebuildIndex(); + } + } + } + + public static class ScheduledPublishing + { + private static bool _suspended; + + public static bool CanRun + { + get { return _suspended == false; } + } + + public static void Suspend() + { + ApplicationContext.Current.ProfilingLogger.Logger.Info(typeof (ScheduledPublishing), "Suspend scheduled publishing."); + _suspended = true; + } + + public static void Resume() + { + ApplicationContext.Current.ProfilingLogger.Logger.Info(typeof (ScheduledPublishing), "Resume scheduled publishing."); + _suspended = false; + } + } + } +} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index ea3a0dfa7c..f8d6f9080e 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -435,6 +435,7 @@ + diff --git a/src/Umbraco.Web/umbraco.presentation/content.cs b/src/Umbraco.Web/umbraco.presentation/content.cs index 54bb1ac2f1..a7d40fa088 100644 --- a/src/Umbraco.Web/umbraco.presentation/content.cs +++ b/src/Umbraco.Web/umbraco.presentation/content.cs @@ -2,6 +2,7 @@ using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics; using System.Globalization; using System.IO; using System.Text; From b02b2a61f038b1f2e0564b6a9025df0fe7634148 Mon Sep 17 00:00:00 2001 From: Stephan Date: Thu, 7 Sep 2017 12:16:04 +0200 Subject: [PATCH 4/8] perfs - deploy context isCancelled --- src/Umbraco.Core/Deploy/IDeployContext.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Umbraco.Core/Deploy/IDeployContext.cs b/src/Umbraco.Core/Deploy/IDeployContext.cs index 7d4066e015..531ed9dae4 100644 --- a/src/Umbraco.Core/Deploy/IDeployContext.cs +++ b/src/Umbraco.Core/Deploy/IDeployContext.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading; namespace Umbraco.Core.Deploy { @@ -38,5 +39,10 @@ namespace Umbraco.Core.Deploy /// The key of the item. /// The item with the specified key and type, if any, else null. T Item(string key) where T : class; + + ///// + ///// Gets the global deployment cancellation token. + ///// + //CancellationToken CancellationToken { get; } } } \ No newline at end of file From d6f8b878d29264d30f55782fb7552cd213ca33e7 Mon Sep 17 00:00:00 2001 From: Stephan Date: Thu, 7 Sep 2017 13:45:56 +0200 Subject: [PATCH 5/8] perfs - cleanup --- src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs index 6fcdf2fcf3..879f74bccd 100644 --- a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs +++ b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs @@ -822,9 +822,6 @@ namespace Umbraco.Web.Cache { var handler = FindHandler(e); if (handler == null) continue; - - ApplicationContext.Current.ProfilingLogger.Logger.Info("Handling " + e.Sender + " " + e.EventName); - handler.Invoke(null, new[] { e.Sender, e.Args }); } } @@ -833,7 +830,7 @@ namespace Umbraco.Web.Cache if (tempContext != null) tempContext.Dispose(); } - + } /// From 7003ef8d26b860b5db7e82ad0c0867683422bb57 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Thu, 7 Sep 2017 20:42:12 +0200 Subject: [PATCH 6/8] Remove circular dependency --- src/Umbraco.Core/Umbraco.Core.csproj | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 77e84da945..8854c3c4ab 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -134,9 +134,6 @@ - - ..\Umbraco.Web.UI\Bin\umbraco.dll - From 96c9ecbdd738e83edb5884596e66014c42aa4509 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 7 Sep 2017 21:39:41 +0200 Subject: [PATCH 7/8] fix calc functions --- src/Umbraco.Web.UI.Client/src/less/application/grid.less | 2 +- src/Umbraco.Web.UI.Client/src/less/canvas-designer.less | 2 +- src/Umbraco.Web.UI.Client/src/less/components/overlays.less | 2 +- src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less | 2 +- src/Umbraco.Web.UI.Client/src/less/sections.less | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/application/grid.less b/src/Umbraco.Web.UI.Client/src/less/application/grid.less index 8348b072b1..f6879bb679 100644 --- a/src/Umbraco.Web.UI.Client/src/less/application/grid.less +++ b/src/Umbraco.Web.UI.Client/src/less/application/grid.less @@ -125,7 +125,7 @@ body { @media (max-width: 500px) { #search-form .form-search { - width: ~"(calc(~'100%' - ~'80px'))"; + width: calc(100% - 80px); } } diff --git a/src/Umbraco.Web.UI.Client/src/less/canvas-designer.less b/src/Umbraco.Web.UI.Client/src/less/canvas-designer.less index e7aa9d859f..095027d86b 100644 --- a/src/Umbraco.Web.UI.Client/src/less/canvas-designer.less +++ b/src/Umbraco.Web.UI.Client/src/less/canvas-designer.less @@ -14,7 +14,7 @@ body { overflow: hidden; height: 100%; width: 100%; - width: ~"(calc(~'100%' - ~'80px'))"; // 80px is the fixed left menu for toggling the different browser sizes + width: calc(100% - 80px); // 80px is the fixed left menu for toggling the different browser sizes position: absolute; padding: 0; margin: 0; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/overlays.less b/src/Umbraco.Web.UI.Client/src/less/components/overlays.less index c7e91c1a44..0a8dfe6f71 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/overlays.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/overlays.less @@ -155,7 +155,7 @@ @media (max-width: 500px) { .umb-overlay.umb-overlay-left { margin-left: 41px; - width: ~"(calc(~'100%' - ~'41px'))"; + width: calc(100% - 41px); } } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less index e053d58a7f..1ca6bf6067 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less @@ -329,7 +329,7 @@ a.umb-package-details__back-link { .umb-package-details__main-content { flex: 1 1 auto; margin-right: 40px; - width: ~"(calc(~'100%' - ~'@{sidebarwidth}' - ~'40px'))"; // Make sure that the main content area doesn't gets affected by inline styling + width: calc(~'100%' - ~'@{sidebarwidth}' - ~'40px'); // Make sure that the main content area doesn't gets affected by inline styling } .umb-package-details__sidebar { diff --git a/src/Umbraco.Web.UI.Client/src/less/sections.less b/src/Umbraco.Web.UI.Client/src/less/sections.less index d73e24e4ea..85a33c84b8 100644 --- a/src/Umbraco.Web.UI.Client/src/less/sections.less +++ b/src/Umbraco.Web.UI.Client/src/less/sections.less @@ -126,7 +126,7 @@ ul.sections li.help { bottom: 0; left: 0; display: block; - width: ~"(calc(~'100%' - ~'5px'))"; //subtract 4px orange border + 1px border-right for sections + width: calc(100% - 5px); //subtract 4px orange border + 1px border-right for sections } ul.sections li.help a { From dad4eafebda1340f92e24f01aeeee893bd882dc1 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Thu, 7 Sep 2017 22:25:45 +0200 Subject: [PATCH 8/8] Enable RDP to figure out gulp build problem --- appveyor.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/appveyor.yml b/appveyor.yml index dc6e22edbf..4c9a33fd23 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,5 +1,9 @@ version: '{build}' shallow_clone: true + +init: + - ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) + build_script: - cmd: >- SET SLN=%CD%