From 20d8656237821a1b98df4b04ea46a8d1571ea6bd Mon Sep 17 00:00:00 2001 From: Stephan Date: Tue, 25 Aug 2015 15:48:12 +0200 Subject: [PATCH] U4-6992 - fix server registration for new LB --- src/Umbraco.Core/ApplicationContext.cs | 21 +- src/Umbraco.Core/Constants-System.cs | 2 + .../Models/IServerRegistration.cs | 7 + .../Models/Rdbms/ServerRegistrationDto.cs | 3 +- src/Umbraco.Core/Models/ServerRegistration.cs | 25 +- .../Factories/ServerRegistrationFactory.cs | 3 +- .../Mappers/ServerRegistrationMapper.cs | 1 + .../AddServerRegistrationColumnsAndLock.cs | 70 ++++ .../ServerRegistrationRepository.cs | 5 +- .../Services/IServerRegistrationService.cs | 18 +- .../Services/ServerRegistrationService.cs | 104 ++++-- src/Umbraco.Core/Sync/ApplicationUrlHelper.cs | 133 ++++++++ .../Sync/ConfigServerRegistrar.cs | 77 ++++- .../Sync/CurrentServerEnvironmentStatus.cs | 28 -- .../Sync/DatabaseServerRegistrar.cs | 21 +- .../Sync/DatabaseServerRegistrarOptions.cs | 2 +- src/Umbraco.Core/Sync/IServerRegistrar.cs | 3 +- src/Umbraco.Core/Sync/IServerRegistrar2.cs | 26 ++ .../Sync/ServerEnvironmentHelper.cs | 131 -------- src/Umbraco.Core/Sync/ServerRole.cs | 28 ++ src/Umbraco.Core/Umbraco.Core.csproj | 6 +- .../ApplicationUrlHelperTests.cs | 303 ++++++++++++++++++ .../ServerEnvironmentHelperTests.cs | 115 ------- src/Umbraco.Tests/Umbraco.Tests.csproj | 2 +- src/Umbraco.Web/Scheduling/LogScrubber.cs | 10 +- .../Scheduling/ScheduledPublishing.cs | 10 +- src/Umbraco.Web/Scheduling/ScheduledTasks.cs | 10 +- .../ServerRegistrationEventHandler.cs | 47 +-- src/Umbraco.Web/UmbracoModule.cs | 33 +- 29 files changed, 837 insertions(+), 407 deletions(-) create mode 100644 src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddServerRegistrationColumnsAndLock.cs create mode 100644 src/Umbraco.Core/Sync/ApplicationUrlHelper.cs delete mode 100644 src/Umbraco.Core/Sync/CurrentServerEnvironmentStatus.cs create mode 100644 src/Umbraco.Core/Sync/IServerRegistrar2.cs delete mode 100644 src/Umbraco.Core/Sync/ServerEnvironmentHelper.cs create mode 100644 src/Umbraco.Core/Sync/ServerRole.cs create mode 100644 src/Umbraco.Tests/ApplicationUrlHelperTests.cs delete mode 100644 src/Umbraco.Tests/ServerEnvironmentHelperTests.cs diff --git a/src/Umbraco.Core/ApplicationContext.cs b/src/Umbraco.Core/ApplicationContext.cs index 7e306a6361..4aef011d05 100644 --- a/src/Umbraco.Core/ApplicationContext.cs +++ b/src/Umbraco.Core/ApplicationContext.cs @@ -2,6 +2,7 @@ using System.Configuration; using System.Threading; using System.Threading.Tasks; +using System.Web; using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Core.ObjectResolution; @@ -259,22 +260,13 @@ namespace Umbraco.Core { get { - // if initialized, return - if (_umbracoApplicationUrl != null) return _umbracoApplicationUrl; - - // try settings - ServerEnvironmentHelper.TrySetApplicationUrlFromSettings(this, ProfilingLogger.Logger, UmbracoConfig.For.UmbracoSettings()); - - // and return what we have, may be null + ApplicationUrlHelper.EnsureApplicationUrl(this); return _umbracoApplicationUrl; } - set - { - _umbracoApplicationUrl = value; - } } - internal string _umbracoApplicationUrl; // internal for tests + // ReSharper disable once InconsistentNaming + internal string _umbracoApplicationUrl; private Lazy _configured; internal MainDom MainDom { get; private set; } @@ -379,6 +371,11 @@ namespace Umbraco.Core internal set { _services = value; } } + internal ServerRole GetCurrentServerRole() + { + var registrar = ServerRegistrarResolver.Current.Registrar as IServerRegistrar2; + return registrar == null ? ServerRole.Unknown : registrar.GetCurrentServerRole(); + } private volatile bool _disposed; private readonly ReaderWriterLockSlim _disposalLocker = new ReaderWriterLockSlim(); diff --git a/src/Umbraco.Core/Constants-System.cs b/src/Umbraco.Core/Constants-System.cs index b72afa265f..82e3a1ff3f 100644 --- a/src/Umbraco.Core/Constants-System.cs +++ b/src/Umbraco.Core/Constants-System.cs @@ -26,6 +26,8 @@ public const int DefaultMediaListViewDataTypeId = -96; public const int DefaultMembersListViewDataTypeId = -97; + // identifiers for lock objects + public const int ServersLock = -331; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/IServerRegistration.cs b/src/Umbraco.Core/Models/IServerRegistration.cs index 9eb6815bbe..58b8e46249 100644 --- a/src/Umbraco.Core/Models/IServerRegistration.cs +++ b/src/Umbraco.Core/Models/IServerRegistration.cs @@ -18,6 +18,13 @@ namespace Umbraco.Core.Models /// bool IsActive { get; set; } + // note: cannot add this because of backward compatibility + // + ///// + ///// Gets or sets a value indicating whether the server is master. + ///// + //bool IsMaster { get; set; } + /// /// Gets the date and time the registration was created. /// diff --git a/src/Umbraco.Core/Models/Rdbms/ServerRegistrationDto.cs b/src/Umbraco.Core/Models/Rdbms/ServerRegistrationDto.cs index b7bdf265ce..aa6906aa59 100644 --- a/src/Umbraco.Core/Models/Rdbms/ServerRegistrationDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/ServerRegistrationDto.cs @@ -33,6 +33,7 @@ namespace Umbraco.Core.Models.Rdbms [Index(IndexTypes.NonClustered)] public bool IsActive { get; set; } - + [Column("isMaster")] + public bool IsMaster { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/ServerRegistration.cs b/src/Umbraco.Core/Models/ServerRegistration.cs index 9a871859b6..cee70893d0 100644 --- a/src/Umbraco.Core/Models/ServerRegistration.cs +++ b/src/Umbraco.Core/Models/ServerRegistration.cs @@ -2,7 +2,6 @@ using System.Globalization; using System.Reflection; using Umbraco.Core.Models.EntityBase; -using Umbraco.Core.Sync; namespace Umbraco.Core.Models { @@ -14,10 +13,12 @@ namespace Umbraco.Core.Models private string _serverAddress; private string _serverIdentity; private bool _isActive; + private bool _isMaster; private static readonly PropertyInfo ServerAddressSelector = ExpressionHelper.GetPropertyInfo(x => x.ServerAddress); private static readonly PropertyInfo ServerIdentitySelector = ExpressionHelper.GetPropertyInfo(x => x.ServerIdentity); private static readonly PropertyInfo IsActiveSelector = ExpressionHelper.GetPropertyInfo(x => x.IsActive); + private static readonly PropertyInfo IsMasterSelector = ExpressionHelper.GetPropertyInfo(x => x.IsMaster); /// /// Initialiazes a new instance of the class. @@ -34,7 +35,8 @@ namespace Umbraco.Core.Models /// The date and time the registration was created. /// The date and time the registration was last accessed. /// A value indicating whether the registration is active. - public ServerRegistration(int id, string serverAddress, string serverIdentity, DateTime registered, DateTime accessed, bool isActive) + /// A value indicating whether the registration is master. + public ServerRegistration(int id, string serverAddress, string serverIdentity, DateTime registered, DateTime accessed, bool isActive, bool isMaster) { UpdateDate = accessed; CreateDate = registered; @@ -43,6 +45,7 @@ namespace Umbraco.Core.Models ServerAddress = serverAddress; ServerIdentity = serverIdentity; IsActive = isActive; + IsMaster = isMaster; } /// @@ -108,6 +111,22 @@ namespace Umbraco.Core.Models } } + /// + /// Gets or sets a value indicating whether the server is master. + /// + public bool IsMaster + { + get { return _isMaster; } + set + { + SetPropertyValueAndDetectChanges(o => + { + _isMaster = value; + return _isMaster; + }, _isMaster, IsMasterSelector); + } + } + /// /// Gets the date and time the registration was created. /// @@ -124,7 +143,7 @@ namespace Umbraco.Core.Models /// public override string ToString() { - return string.Format("{{\"{0}\", \"{1}\", {2}active}}", ServerAddress, ServerIdentity, IsActive ? "" : "!"); + return string.Format("{{\"{0}\", \"{1}\", {2}active, {3}master}}", ServerAddress, ServerIdentity, IsActive ? "" : "!", IsMaster ? "" : "!"); } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Factories/ServerRegistrationFactory.cs b/src/Umbraco.Core/Persistence/Factories/ServerRegistrationFactory.cs index 9ecc02e213..9c315aef46 100644 --- a/src/Umbraco.Core/Persistence/Factories/ServerRegistrationFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/ServerRegistrationFactory.cs @@ -7,7 +7,7 @@ namespace Umbraco.Core.Persistence.Factories { public ServerRegistration BuildEntity(ServerRegistrationDto dto) { - var model = new ServerRegistration(dto.Id, dto.ServerAddress, dto.ServerIdentity, dto.DateRegistered, dto.DateAccessed, dto.IsActive); + var model = new ServerRegistration(dto.Id, dto.ServerAddress, dto.ServerIdentity, dto.DateRegistered, dto.DateAccessed, dto.IsActive, dto.IsMaster); //on initial construction we don't want to have dirty properties tracked // http://issues.umbraco.org/issue/U4-1946 model.ResetDirtyProperties(false); @@ -21,6 +21,7 @@ namespace Umbraco.Core.Persistence.Factories ServerAddress = entity.ServerAddress, DateRegistered = entity.CreateDate, IsActive = entity.IsActive, + IsMaster = ((ServerRegistration) entity).IsMaster, DateAccessed = entity.UpdateDate, ServerIdentity = entity.ServerIdentity }; diff --git a/src/Umbraco.Core/Persistence/Mappers/ServerRegistrationMapper.cs b/src/Umbraco.Core/Persistence/Mappers/ServerRegistrationMapper.cs index e532db18aa..54d7e8cdc1 100644 --- a/src/Umbraco.Core/Persistence/Mappers/ServerRegistrationMapper.cs +++ b/src/Umbraco.Core/Persistence/Mappers/ServerRegistrationMapper.cs @@ -30,6 +30,7 @@ namespace Umbraco.Core.Persistence.Mappers { CacheMap(src => src.Id, dto => dto.Id); CacheMap(src => src.IsActive, dto => dto.IsActive); + CacheMap(src => src.IsMaster, dto => dto.IsMaster); CacheMap(src => src.ServerAddress, dto => dto.ServerAddress); CacheMap(src => src.CreateDate, dto => dto.DateRegistered); CacheMap(src => src.UpdateDate, dto => dto.DateAccessed); diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddServerRegistrationColumnsAndLock.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddServerRegistrationColumnsAndLock.cs new file mode 100644 index 0000000000..0a29fbb6ef --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddServerRegistrationColumnsAndLock.cs @@ -0,0 +1,70 @@ +using System; +using System.Linq; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; +using Umbraco.Core.Models.Rdbms; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZero +{ + [Migration("7.3.0", 17, GlobalSettings.UmbracoMigrationName)] + public class AddServerRegistrationColumnsAndLock : MigrationBase + { + public AddServerRegistrationColumnsAndLock(ISqlSyntaxProvider sqlSyntax, ILogger logger) + : base(sqlSyntax, logger) + { } + + public override void Up() + { + // don't execute if the column is already there + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToArray(); + if (columns.Any(x => x.TableName.InvariantEquals("umbracoServer") && x.ColumnName.InvariantEquals("isMaster")) == false) + { + Create.Column("isMaster").OnTable("umbracoServer").AsBoolean().NotNullable().WithDefaultValue(0); + } + + // wrap in a transaction so that everything runs on the same connection + // and the IDENTITY_INSERT stuff is effective for all inserts. + using (var tr = Context.Database.GetTransaction()) + { + // turn on identity insert if db provider is not mysql + if (SqlSyntax.SupportsIdentityInsert()) + Context.Database.Execute(new Sql(string.Format("SET IDENTITY_INSERT {0} ON", SqlSyntax.GetQuotedTableName("umbracoNode")))); + + InsertLockObject(Constants.System.ServersLock, "0AF5E610-A310-4B6F-925F-E928D5416AF7", "LOCK: Servers"); + + // turn off identity insert if db provider is not mysql + if (SqlSyntax.SupportsIdentityInsert()) + Context.Database.Execute(new Sql(string.Format("SET IDENTITY_INSERT {0} OFF", SqlSyntax.GetQuotedTableName("umbracoNode")))); + + tr.Complete(); + } + } + + public override void Down() + { + // not implemented + } + + private void InsertLockObject(int id, string uniqueId, string text) + { + var exists = Context.Database.Exists(id); + if (exists) return; + + Context.Database.Insert("umbracoNode", "id", false, new NodeDto + { + NodeId = id, + Trashed = false, + ParentId = -1, + UserId = 0, + Level = 1, + Path = "-1," + id, + SortOrder = 0, + UniqueId = new Guid(uniqueId), + Text = text, + NodeObjectType = new Guid(Constants.ObjectTypes.LockObject), + CreateDate = DateTime.Now + }); + } + } +} diff --git a/src/Umbraco.Core/Persistence/Repositories/ServerRegistrationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ServerRegistrationRepository.cs index eeadceebdb..fc16009e6c 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ServerRegistrationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ServerRegistrationRepository.cs @@ -117,10 +117,9 @@ namespace Umbraco.Core.Persistence.Repositories public void DeactiveStaleServers(TimeSpan staleTimeout) { - var timeoutDate = DateTime.UtcNow.Subtract(staleTimeout); + var timeoutDate = DateTime.Now.Subtract(staleTimeout); - Database.Update("SET isActive=0 WHERE lastNotifiedDate < @timeoutDate", new { timeoutDate = timeoutDate }); + Database.Update("SET isActive=0, isMaster=0 WHERE lastNotifiedDate < @timeoutDate", new { timeoutDate = timeoutDate }); } - } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/IServerRegistrationService.cs b/src/Umbraco.Core/Services/IServerRegistrationService.cs index c0bacb5c40..d581b86f15 100644 --- a/src/Umbraco.Core/Services/IServerRegistrationService.cs +++ b/src/Umbraco.Core/Services/IServerRegistrationService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Umbraco.Core.Models; +using Umbraco.Core.Sync; namespace Umbraco.Core.Services { @@ -29,7 +30,22 @@ namespace Umbraco.Core.Services /// /// Return all active servers. /// - /// + /// All active servers. IEnumerable GetActiveServers(); + + // note: cannot add this because of backward compatibility + // + ///// + ///// Gets the current server identity. + ///// + //string CurrentServerIdentity { get; } + + // note: cannot add this because of backward compatibility + // + ///// + ///// Gets the role of the current server. + ///// + ///// The role of the current server. + //ServerRole GetCurrentServerRole(); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/ServerRegistrationService.cs b/src/Umbraco.Core/Services/ServerRegistrationService.cs index fac56499fd..81e6a39add 100644 --- a/src/Umbraco.Core/Services/ServerRegistrationService.cs +++ b/src/Umbraco.Core/Services/ServerRegistrationService.cs @@ -1,12 +1,15 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Web; using Umbraco.Core.Events; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.UnitOfWork; +using Umbraco.Core.Sync; namespace Umbraco.Core.Services { @@ -15,6 +18,13 @@ namespace Umbraco.Core.Services /// public sealed class ServerRegistrationService : RepositoryService, IServerRegistrationService { + private readonly static string CurrentServerIdentityValue = NetworkHelper.MachineName // eg DOMAIN\SERVER + + "/" + HttpRuntime.AppDomainAppId; // eg /LM/S3SVC/11/ROOT + + private static readonly int[] LockingRepositoryIds = { Constants.System.ServersLock }; + private ServerRole _currentServerRole = ServerRole.Unknown; + private readonly LockingRepository _lrepo; + /// /// Initializes a new instance of the class. /// @@ -24,7 +34,12 @@ namespace Umbraco.Core.Services /// public ServerRegistrationService(IDatabaseUnitOfWorkProvider uowProvider, RepositoryFactory repositoryFactory, ILogger logger, IEventMessagesFactory eventMessagesFactory) : base(uowProvider, repositoryFactory, logger, eventMessagesFactory) - { } + { + _lrepo = new LockingRepository(UowProvider, + x => RepositoryFactory.CreateServerRegistrationRepository(x), + LockingRepositoryIds, LockingRepositoryIds); + + } /// /// Touches a server to mark it as active; deactivate stale servers. @@ -34,29 +49,42 @@ namespace Umbraco.Core.Services /// The time after which a server is considered stale. public void TouchServer(string serverAddress, string serverIdentity, TimeSpan staleTimeout) { - var uow = UowProvider.GetUnitOfWork(); - using (var repo = RepositoryFactory.CreateServerRegistrationRepository(uow)) + _lrepo.WithWriteLocked(xr => { - var query = Query.Builder.Where(x => x.ServerIdentity.ToUpper() == serverIdentity.ToUpper()); - var server = repo.GetByQuery(query).FirstOrDefault(); + var regs = xr.Repository.GetAll().ToArray(); // faster to query only once + var hasMaster = regs.Any(x => ((ServerRegistration)x).IsMaster); + var iserver = regs.FirstOrDefault(x => x.ServerIdentity.InvariantEquals(serverIdentity)); + var server = iserver as ServerRegistration; // because IServerRegistration is missing IsMaster + var hasServer = server != null; + if (server == null) { - server = new ServerRegistration(serverAddress, serverIdentity, DateTime.UtcNow) - { - IsActive = true - }; + server = new ServerRegistration(serverAddress, serverIdentity, DateTime.Now); } else { server.ServerAddress = serverAddress; // should not really change but it might! - server.UpdateDate = DateTime.UtcNow; // stick with Utc dates since these might be globally distributed - server.IsActive = true; + server.UpdateDate = DateTime.Now; } - repo.AddOrUpdate(server); - uow.Commit(); - repo.DeactiveStaleServers(staleTimeout); - } + server.IsActive = true; + if (hasMaster == false) + server.IsMaster = true; + + xr.Repository.AddOrUpdate(server); + xr.UnitOfWork.Commit(); + xr.Repository.DeactiveStaleServers(staleTimeout); + + // default role is single server + _currentServerRole = ServerRole.Single; + + // if registrations contain more than 0/1 server, role is master or slave + // compare to 0 or 1 depending on whether regs already contains the server + if (regs.Length > (hasServer ? 1 : 0)) + _currentServerRole = server.IsMaster + ? ServerRole.Master + : ServerRole.Slave; + }); } /// @@ -65,18 +93,17 @@ namespace Umbraco.Core.Services /// The server unique identity. public void DeactiveServer(string serverIdentity) { - var uow = UowProvider.GetUnitOfWork(); - using (var repo = RepositoryFactory.CreateServerRegistrationRepository(uow)) + _lrepo.WithWriteLocked(xr => { var query = Query.Builder.Where(x => x.ServerIdentity.ToUpper() == serverIdentity.ToUpper()); - var server = repo.GetByQuery(query).FirstOrDefault(); - if (server != null) - { - server.IsActive = false; - repo.AddOrUpdate(server); - uow.Commit(); - } - } + var iserver = xr.Repository.GetByQuery(query).FirstOrDefault(); + var server = iserver as ServerRegistration; // because IServerRegistration is missing IsMaster + if (server == null) return; + + server.IsActive = false; + server.IsMaster = false; + xr.Repository.AddOrUpdate(server); + }); } /// @@ -85,11 +112,7 @@ namespace Umbraco.Core.Services /// The time after which a server is considered stale. public void DeactiveStaleServers(TimeSpan staleTimeout) { - var uow = UowProvider.GetUnitOfWork(); - using (var repo = RepositoryFactory.CreateServerRegistrationRepository(uow)) - { - repo.DeactiveStaleServers(staleTimeout); - } + _lrepo.WithWriteLocked(xr => xr.Repository.DeactiveStaleServers(staleTimeout)); } /// @@ -98,12 +121,25 @@ namespace Umbraco.Core.Services /// public IEnumerable GetActiveServers() { - var uow = UowProvider.GetUnitOfWork(); - using (var repo = RepositoryFactory.CreateServerRegistrationRepository(uow)) + return _lrepo.WithReadLocked(xr => { var query = Query.Builder.Where(x => x.IsActive); - return repo.GetByQuery(query).ToArray(); - } + return xr.Repository.GetByQuery(query).ToArray(); + }); + } + + /// + /// Gets the local server identity. + /// + public string CurrentServerIdentity { get { return CurrentServerIdentityValue; } } + + /// + /// Gets the role of the current server. + /// + /// The role of the current server. + public ServerRole GetCurrentServerRole() + { + return _currentServerRole; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Sync/ApplicationUrlHelper.cs b/src/Umbraco.Core/Sync/ApplicationUrlHelper.cs new file mode 100644 index 0000000000..16161f4b3a --- /dev/null +++ b/src/Umbraco.Core/Sync/ApplicationUrlHelper.cs @@ -0,0 +1,133 @@ +using System; +using System.Web; +using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.IO; + +namespace Umbraco.Core.Sync +{ + /// + /// A helper used to determine the current server umbraco application url. + /// + public static class ApplicationUrlHelper + { + // because we cannot logger.Info because type is static + private static readonly Type TypeOfApplicationUrlHelper = typeof(ApplicationUrlHelper); + + /// + /// Gets or sets a custom provider for the umbraco application url. + /// + /// Receives the current request as a parameter, and it may be null. Must return a properly + /// formatted url with scheme and umbraco dir and no trailing slash eg "http://www.mysite.com/umbraco", + /// or null. To be used in auto-load-balancing scenarios where the application url is not + /// in config files but is determined programmatically. + public static Func ApplicationUrlProvider { get; set; } + + // request: will be null if called from ApplicationContext + // settings: for unit tests only + internal static void EnsureApplicationUrl(ApplicationContext appContext, HttpRequestBase request = null, IUmbracoSettingsSection settings = null) + { + // if initialized, return + if (appContext._umbracoApplicationUrl != null) return; + + var logger = appContext.ProfilingLogger.Logger; + + // try settings and IServerRegistrar + if (TrySetApplicationUrl(appContext, settings ?? UmbracoConfig.For.UmbracoSettings())) + return; + + // try custom provider + if (ApplicationUrlProvider != null) + { + var url = ApplicationUrlProvider(request); + if (url.IsNullOrWhiteSpace() == false) + { + appContext._umbracoApplicationUrl = url.TrimEnd('/'); + logger.Info(TypeOfApplicationUrlHelper, "ApplicationUrl: " + appContext.UmbracoApplicationUrl + " (provider)"); + return; + } + } + + // last chance, + // use the current request as application url + if (request == null) return; + SetApplicationUrlFromCurrentRequest(appContext, request); + } + + // internal for tests + internal static bool TrySetApplicationUrl(ApplicationContext appContext, IUmbracoSettingsSection settings) + { + var logger = appContext.ProfilingLogger.Logger; + + // try umbracoSettings:settings/web.routing/@umbracoApplicationUrl + // which is assumed to: + // - end with SystemDirectories.Umbraco + // - contain a scheme + // - end or not with a slash, it will be taken care of + // eg "http://www.mysite.com/umbraco" + var url = settings.WebRouting.UmbracoApplicationUrl; + if (url.IsNullOrWhiteSpace() == false) + { + appContext._umbracoApplicationUrl = url.TrimEnd('/'); + logger.Info(TypeOfApplicationUrlHelper, "ApplicationUrl: " + appContext.UmbracoApplicationUrl + " (using web.routing/@umbracoApplicationUrl)"); + return true; + } + + // try umbracoSettings:settings/scheduledTasks/@baseUrl + // which is assumed to: + // - end with SystemDirectories.Umbraco + // - NOT contain any scheme (because, legacy) + // - end or not with a slash, it will be taken care of + // eg "mysite.com/umbraco" + url = settings.ScheduledTasks.BaseUrl; + if (url.IsNullOrWhiteSpace() == false) + { + var ssl = GlobalSettings.UseSSL ? "s" : ""; + url = "http" + ssl + "://" + url; + appContext._umbracoApplicationUrl = url.TrimEnd('/'); + logger.Info(TypeOfApplicationUrlHelper, "ApplicationUrl: " + appContext.UmbracoApplicationUrl + " (using scheduledTasks/@baseUrl)"); + return true; + } + + // try the server registrar + // which is assumed to return a url that: + // - end with SystemDirectories.Umbraco + // - contain a scheme + // - end or not with a slash, it will be taken care of + // eg "http://www.mysite.com/umbraco" + var registrar = ServerRegistrarResolver.Current.Registrar as IServerRegistrar2; + url = registrar == null ? null : registrar.GetCurrentServerUmbracoApplicationUrl(); + if (url.IsNullOrWhiteSpace() == false) + { + appContext._umbracoApplicationUrl = url.TrimEnd('/'); + logger.Info(TypeOfApplicationUrlHelper, "ApplicationUrl: " + appContext.UmbracoApplicationUrl + " (IServerRegistrar)"); + return true; + } + + // else give up... + return false; + } + + private static void SetApplicationUrlFromCurrentRequest(ApplicationContext appContext, HttpRequestBase request) + { + var logger = appContext.ProfilingLogger.Logger; + + // if (HTTP and SSL not required) or (HTTPS and SSL required), + // use ports from request + // otherwise, + // if non-standard ports used, + // user may need to set umbracoApplicationUrl manually per + // http://our.umbraco.org/documentation/Using-Umbraco/Config-files/umbracoSettings/#ScheduledTasks + var port = (request.IsSecureConnection == false && GlobalSettings.UseSSL == false) + || (request.IsSecureConnection && GlobalSettings.UseSSL) + ? ":" + request.ServerVariables["SERVER_PORT"] + : ""; + + var ssl = GlobalSettings.UseSSL ? "s" : ""; // force, whatever the first request + var url = "http" + ssl + "://" + request.ServerVariables["SERVER_NAME"] + port + IOHelper.ResolveUrl(SystemDirectories.Umbraco); + + appContext._umbracoApplicationUrl = url.TrimEnd('/'); + logger.Info(TypeOfApplicationUrlHelper, "ApplicationUrl: " + appContext.UmbracoApplicationUrl + " (UmbracoModule request)"); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Sync/ConfigServerRegistrar.cs b/src/Umbraco.Core/Sync/ConfigServerRegistrar.cs index 06cf03f7ce..8245491d97 100644 --- a/src/Umbraco.Core/Sync/ConfigServerRegistrar.cs +++ b/src/Umbraco.Core/Sync/ConfigServerRegistrar.cs @@ -1,7 +1,9 @@ using System.Collections.Generic; using System.Linq; +using System.Web; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.IO; namespace Umbraco.Core.Sync { @@ -9,27 +11,84 @@ namespace Umbraco.Core.Sync /// Provides server registrations to the distributed cache by reading the legacy Xml configuration /// in umbracoSettings to get the list of (manually) configured server nodes. /// - internal class ConfigServerRegistrar : IServerRegistrar + internal class ConfigServerRegistrar : IServerRegistrar2 { private readonly List _addresses; + private readonly ServerRole _serverRole; + private readonly string _umbracoApplicationUrl; public ConfigServerRegistrar() - : this(UmbracoConfig.For.UmbracoSettings().DistributedCall.Servers) + : this(UmbracoConfig.For.UmbracoSettings().DistributedCall) { } - internal ConfigServerRegistrar(IEnumerable servers) + // for tests + internal ConfigServerRegistrar(IDistributedCallSection settings) { - _addresses = servers == null - ? new List() - : servers - .Select(x => new ConfigServerAddress(x)) - .Cast() - .ToList(); + if (settings.Enabled == false) + { + _addresses = new List(); + _serverRole = ServerRole.Single; + _umbracoApplicationUrl = null; // unspecified + return; + } + + var serversA = settings.Servers.ToArray(); + + _addresses = serversA + .Select(x => new ConfigServerAddress(x)) + .Cast() + .ToList(); + + if (serversA.Length == 0) + { + _serverRole = ServerRole.Unknown; // config error, actually + } + else + { + var master = serversA[0]; // first one is master + var appId = master.AppId; + var serverName = master.ServerName; + + if (appId.IsNullOrWhiteSpace() && serverName.IsNullOrWhiteSpace()) + _serverRole = ServerRole.Unknown; // config error, actually + else + _serverRole = IsCurrentServer(appId, serverName) + ? ServerRole.Master + : ServerRole.Slave; + } + + var currentServer = serversA.FirstOrDefault(x => IsCurrentServer(x.AppId, x.ServerName)); + if (currentServer != null) + { + // match, use the configured url + _umbracoApplicationUrl = string.Format("{0}://{1}:{2}/{3}", + currentServer.ForceProtocol.IsNullOrWhiteSpace() ? "http" : currentServer.ForceProtocol, + currentServer.ServerAddress, + currentServer.ForcePortnumber.IsNullOrWhiteSpace() ? "80" : currentServer.ForcePortnumber, + IOHelper.ResolveUrl(SystemDirectories.Umbraco).TrimStart('/')); + } + } + + private static bool IsCurrentServer(string appId, string serverName) + { + // match by appId or computer name + return (appId.IsNullOrWhiteSpace() == false && appId.Trim().InvariantEquals(HttpRuntime.AppDomainAppId)) + || (serverName.IsNullOrWhiteSpace() == false && serverName.Trim().InvariantEquals(NetworkHelper.MachineName)); } public IEnumerable Registrations { get { return _addresses; } } + + public ServerRole GetCurrentServerRole() + { + return _serverRole; + } + + public string GetCurrentServerUmbracoApplicationUrl() + { + return _umbracoApplicationUrl; + } } } diff --git a/src/Umbraco.Core/Sync/CurrentServerEnvironmentStatus.cs b/src/Umbraco.Core/Sync/CurrentServerEnvironmentStatus.cs deleted file mode 100644 index 95305b7cdc..0000000000 --- a/src/Umbraco.Core/Sync/CurrentServerEnvironmentStatus.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace Umbraco.Core.Sync -{ - /// - /// The current status of the server in the Umbraco environment - /// - internal enum CurrentServerEnvironmentStatus - { - /// - /// If the current server is detected as the 'master' server when configured in a load balanced scenario - /// - Master, - - /// - /// If the current server is detected as a 'slave' server when configured in a load balanced scenario - /// - Slave, - - /// - /// If the current server cannot be detected as a 'slave' or 'master' when configured in a load balanced scenario - /// - Unknown, - - /// - /// If load balancing is not enabled and this is the only server in the umbraco environment - /// - Single - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Sync/DatabaseServerRegistrar.cs b/src/Umbraco.Core/Sync/DatabaseServerRegistrar.cs index a200b8e2ab..8ac40c1a51 100644 --- a/src/Umbraco.Core/Sync/DatabaseServerRegistrar.cs +++ b/src/Umbraco.Core/Sync/DatabaseServerRegistrar.cs @@ -7,7 +7,7 @@ namespace Umbraco.Core.Sync /// /// A registrar that stores registered server nodes in the database. /// - public sealed class DatabaseServerRegistrar : IServerRegistrar + public sealed class DatabaseServerRegistrar : IServerRegistrar2 { private readonly Lazy _registrationService; @@ -37,5 +37,24 @@ namespace Umbraco.Core.Sync { get { return _registrationService.Value.GetActiveServers(); } } + + /// + /// Gets the role of the current server in the application environment. + /// + public ServerRole GetCurrentServerRole() + { + var service = _registrationService.Value as ServerRegistrationService; + return service.GetCurrentServerRole(); + } + + /// + /// Gets the current umbraco application url. + /// + public string GetCurrentServerUmbracoApplicationUrl() + { + // this registrar does not provide the umbraco application url + return null; + } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Sync/DatabaseServerRegistrarOptions.cs b/src/Umbraco.Core/Sync/DatabaseServerRegistrarOptions.cs index 4ee7fec371..33ab1c8f57 100644 --- a/src/Umbraco.Core/Sync/DatabaseServerRegistrarOptions.cs +++ b/src/Umbraco.Core/Sync/DatabaseServerRegistrarOptions.cs @@ -12,7 +12,7 @@ namespace Umbraco.Core.Sync /// public DatabaseServerRegistrarOptions() { - StaleServerTimeout = new TimeSpan(1,0,0); // 1 day + StaleServerTimeout = TimeSpan.FromMinutes(2); // 2 minutes ThrottleSeconds = 30; // 30 seconds } diff --git a/src/Umbraco.Core/Sync/IServerRegistrar.cs b/src/Umbraco.Core/Sync/IServerRegistrar.cs index 5f63440859..821f42ffbf 100644 --- a/src/Umbraco.Core/Sync/IServerRegistrar.cs +++ b/src/Umbraco.Core/Sync/IServerRegistrar.cs @@ -5,11 +5,12 @@ namespace Umbraco.Core.Sync /// /// Provides server registrations to the distributed cache. /// + /// You should implement IServerRegistrar2 instead. public interface IServerRegistrar { /// /// Gets the server registrations. /// - IEnumerable Registrations { get; } + IEnumerable Registrations { get; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Sync/IServerRegistrar2.cs b/src/Umbraco.Core/Sync/IServerRegistrar2.cs new file mode 100644 index 0000000000..4eb9743ccc --- /dev/null +++ b/src/Umbraco.Core/Sync/IServerRegistrar2.cs @@ -0,0 +1,26 @@ +namespace Umbraco.Core.Sync +{ + /// + /// Provides server registrations to the distributed cache. + /// + /// This interface exists because IServerRegistrar could not be modified + /// for backward compatibility reasons - but IServerRegistrar is broken because it + /// does not support server role management. So ppl should really implement + /// IServerRegistrar2, and the two interfaces will get merged in v8. + public interface IServerRegistrar2 : IServerRegistrar + { + /// + /// Gets the role of the current server in the application environment. + /// + ServerRole GetCurrentServerRole(); + + /// + /// Gets the current umbraco application url. + /// + /// + /// If the registrar does not provide the umbraco application url, should return null. + /// Must return null, or a url that ends with SystemDirectories.Umbraco, and contains a scheme, eg "http://www.mysite.com/umbraco". + /// + string GetCurrentServerUmbracoApplicationUrl(); + } +} diff --git a/src/Umbraco.Core/Sync/ServerEnvironmentHelper.cs b/src/Umbraco.Core/Sync/ServerEnvironmentHelper.cs deleted file mode 100644 index e79a0f9209..0000000000 --- a/src/Umbraco.Core/Sync/ServerEnvironmentHelper.cs +++ /dev/null @@ -1,131 +0,0 @@ -using System.Linq; -using System.Web; -using Umbraco.Core.Configuration; -using Umbraco.Core.Configuration.UmbracoSettings; -using Umbraco.Core.IO; -using Umbraco.Core.Logging; - -namespace Umbraco.Core.Sync -{ - /// - /// A helper used to determine the current server environment status - /// - internal static class ServerEnvironmentHelper - { - public static void TrySetApplicationUrlFromSettings(ApplicationContext appContext, ILogger logger, IUmbracoSettingsSection settings) - { - // try umbracoSettings:settings/web.routing/@umbracoApplicationUrl - // which is assumed to: - // - end with SystemDirectories.Umbraco - // - contain a scheme - // - end or not with a slash, it will be taken care of - // eg "http://www.mysite.com/umbraco" - var url = settings.WebRouting.UmbracoApplicationUrl; - if (url.IsNullOrWhiteSpace() == false) - { - appContext.UmbracoApplicationUrl = url.TrimEnd('/'); - logger.Info("ApplicationUrl: " + appContext.UmbracoApplicationUrl + " (using web.routing/@umbracoApplicationUrl)"); - return; - } - - // try umbracoSettings:settings/scheduledTasks/@baseUrl - // which is assumed to: - // - end with SystemDirectories.Umbraco - // - NOT contain any scheme (because, legacy) - // - end or not with a slash, it will be taken care of - // eg "mysite.com/umbraco" - url = settings.ScheduledTasks.BaseUrl; - if (url.IsNullOrWhiteSpace() == false) - { - var ssl = GlobalSettings.UseSSL ? "s" : ""; - url = "http" + ssl + "://" + url; - appContext.UmbracoApplicationUrl = url.TrimEnd('/'); - logger.Info("ApplicationUrl: " + appContext.UmbracoApplicationUrl + " (using scheduledTasks/@baseUrl)"); - return; - } - - // try servers - var status = GetStatus(settings); - if (status == CurrentServerEnvironmentStatus.Single) - return; - - // no server, nothing we can do - var servers = settings.DistributedCall.Servers.ToArray(); - if (servers.Length == 0) - return; - - // we have servers, look for this server - foreach (var server in servers) - { - var appId = server.AppId; - var serverName = server.ServerName; - - // skip if no data - if (appId.IsNullOrWhiteSpace() && serverName.IsNullOrWhiteSpace()) - continue; - - // if this server, build and return the url - if ((appId.IsNullOrWhiteSpace() == false && appId.Trim().InvariantEquals(HttpRuntime.AppDomainAppId)) - || (serverName.IsNullOrWhiteSpace() == false && serverName.Trim().InvariantEquals(NetworkHelper.MachineName))) - { - // match by appId or computer name, return the url configured - url = string.Format("{0}://{1}:{2}/{3}", - server.ForceProtocol.IsNullOrWhiteSpace() ? "http" : server.ForceProtocol, - server.ServerAddress, - server.ForcePortnumber.IsNullOrWhiteSpace() ? "80" : server.ForcePortnumber, - IOHelper.ResolveUrl(SystemDirectories.Umbraco).TrimStart('/')); - - appContext.UmbracoApplicationUrl = url.TrimEnd('/'); - logger.Info("ApplicationUrl: " + appContext.UmbracoApplicationUrl + " (using distributedCall/servers)"); - } - } - } - - /// - /// Returns the current environment status for the current server - /// - /// - public static CurrentServerEnvironmentStatus GetStatus(IUmbracoSettingsSection settings) - { - if (settings.DistributedCall.Enabled == false) - { - return CurrentServerEnvironmentStatus.Single; - } - - var servers = settings.DistributedCall.Servers.ToArray(); - - if (servers.Any() == false) - { - return CurrentServerEnvironmentStatus.Unknown; - } - - var master = servers.FirstOrDefault(); - - if (master == null) - { - return CurrentServerEnvironmentStatus.Unknown; - } - - //we determine master/slave based on the first server registered - //TODO: In v7 we have publicized ServerRegisterResolver - we won't be able to determine this based on that - // but we'd need to change the IServerAddress interfaces which is breaking. - - var appId = master.AppId; - var serverName = master.ServerName; - - if (appId.IsNullOrWhiteSpace() && serverName.IsNullOrWhiteSpace()) - { - return CurrentServerEnvironmentStatus.Unknown; - } - - if ((appId.IsNullOrWhiteSpace() == false && appId.Trim().InvariantEquals(HttpRuntime.AppDomainAppId)) - || (serverName.IsNullOrWhiteSpace() == false && serverName.Trim().InvariantEquals(NetworkHelper.MachineName))) - { - //match by appdid or server name! - return CurrentServerEnvironmentStatus.Master; - } - - return CurrentServerEnvironmentStatus.Slave; - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Sync/ServerRole.cs b/src/Umbraco.Core/Sync/ServerRole.cs new file mode 100644 index 0000000000..cbc121c4bc --- /dev/null +++ b/src/Umbraco.Core/Sync/ServerRole.cs @@ -0,0 +1,28 @@ +namespace Umbraco.Core.Sync +{ + /// + /// The role of a server in an application environment. + /// + public enum ServerRole : byte + { + /// + /// The server role is unknown. + /// + Unknown = 0, + + /// + /// The server is the single server of a single-server environment. + /// + Single = 1, + + /// + /// In a multi-servers environment, the server is a slave server. + /// + Slave = 2, + + /// + /// In a multi-servers environment, the server is the master server. + /// + Master = 3 + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 34f94e9f80..ddc278d026 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -401,6 +401,7 @@ + @@ -1257,13 +1258,14 @@ - + + - + diff --git a/src/Umbraco.Tests/ApplicationUrlHelperTests.cs b/src/Umbraco.Tests/ApplicationUrlHelperTests.cs new file mode 100644 index 0000000000..340467d07c --- /dev/null +++ b/src/Umbraco.Tests/ApplicationUrlHelperTests.cs @@ -0,0 +1,303 @@ +using System.Configuration; +using System.IO; +using System.Linq; +using Moq; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.Logging; +using Umbraco.Core.ObjectResolution; +using Umbraco.Core.Profiling; +using Umbraco.Core.Sync; +using Umbraco.Tests.TestHelpers; + +namespace Umbraco.Tests +{ + [TestFixture] + public class ApplicationUrlHelperTests + { + private ILogger _logger; + + // note: in tests, read appContext._umbracoApplicationUrl and not the property, + // because reading the property does run some code, as long as the field is null. + + [TestFixtureSetUp] + public void InitializeFixture() + { + _logger = new Logger(new FileInfo(TestHelper.MapPathForTest("~/unit-test-log4net.config"))); + } + + private static void Initialize(IUmbracoSettingsSection settings) + { + ServerRegistrarResolver.Current = new ServerRegistrarResolver(new ConfigServerRegistrar(settings.DistributedCall)); + Resolution.Freeze(); + } + + [TearDown] + public void Reset() + { + ServerRegistrarResolver.Reset(); + } + + [Test] + public void NoApplicationUrlByDefault() + { + var appCtx = new ApplicationContext(CacheHelper.CreateDisabledCacheHelper(), + new ProfilingLogger(Mock.Of(), Mock.Of())); + Assert.IsNull(appCtx._umbracoApplicationUrl); + } + + [Test] + public void SetApplicationUrlViaProvider() + { + // no applicable settings, but a provider + + var settings = Mock.Of(section => + section.DistributedCall == Mock.Of(callSection => callSection.Servers == Enumerable.Empty()) + && section.WebRouting == Mock.Of(wrSection => wrSection.UmbracoApplicationUrl == (string)null) + && section.ScheduledTasks == Mock.Of()); + + Initialize(settings); + + var appCtx = new ApplicationContext(CacheHelper.CreateDisabledCacheHelper(), + new ProfilingLogger(Mock.Of(), Mock.Of())); + + ConfigurationManager.AppSettings.Set("umbracoUseSSL", "true"); // does not make a diff here + + ApplicationUrlHelper.ApplicationUrlProvider = request => "http://server1.com/umbraco"; + ApplicationUrlHelper.EnsureApplicationUrl(appCtx, settings: settings); + + Assert.AreEqual("http://server1.com/umbraco", appCtx._umbracoApplicationUrl); + } + + [Test] + public void SetApplicationUrlWhenNoSettings() + { + // no applicable settings, cannot set url + + var settings = Mock.Of(section => + section.DistributedCall == Mock.Of(callSection => callSection.Servers == Enumerable.Empty()) + && section.WebRouting == Mock.Of(wrSection => wrSection.UmbracoApplicationUrl == (string) null) + && section.ScheduledTasks == Mock.Of()); + + Initialize(settings); + + var appCtx = new ApplicationContext(CacheHelper.CreateDisabledCacheHelper(), + new ProfilingLogger(Mock.Of(), Mock.Of())); + + ConfigurationManager.AppSettings.Set("umbracoUseSSL", "true"); // does not make a diff here + + ApplicationUrlHelper.TrySetApplicationUrl(appCtx, settings); + + // still NOT set + Assert.IsNull(appCtx._umbracoApplicationUrl); + } + + [Test] + public void SetApplicationUrlFromDcSettingsSsl1() + { + // set from distributed call settings + // first server is master server + + var settings = Mock.Of(section => + section.DistributedCall == Mock.Of(callSection => callSection.Enabled == true && callSection.Servers == new IServer[] + { + Mock.Of(server => server.ServerName == NetworkHelper.MachineName && server.ServerAddress == "server1.com"), + Mock.Of(server => server.ServerName == "ANOTHERNAME" && server.ServerAddress == "server2.com"), + }) + && section.WebRouting == Mock.Of(wrSection => wrSection.UmbracoApplicationUrl == (string)null) + && section.ScheduledTasks == Mock.Of(tasksSection => tasksSection.BaseUrl == (string)null)); + + Initialize(settings); + + var appCtx = new ApplicationContext(CacheHelper.CreateDisabledCacheHelper(), + new ProfilingLogger(Mock.Of(), Mock.Of())); + + ConfigurationManager.AppSettings.Set("umbracoUseSSL", "true"); + + ApplicationUrlHelper.TrySetApplicationUrl(appCtx, settings); + + Assert.AreEqual("http://server1.com:80/umbraco", appCtx._umbracoApplicationUrl); + + var registrar = ServerRegistrarResolver.Current.Registrar as IServerRegistrar2; + var role = registrar.GetCurrentServerRole(); + Assert.AreEqual(ServerRole.Master, role); + } + + [Test] + public void SetApplicationUrlFromDcSettingsSsl2() + { + // set from distributed call settings + // other servers are slave servers + + var settings = Mock.Of(section => + section.DistributedCall == Mock.Of(callSection => callSection.Enabled == true && callSection.Servers == new IServer[] + { + Mock.Of(server => server.ServerName == "ANOTHERNAME" && server.ServerAddress == "server2.com"), + Mock.Of(server => server.ServerName == NetworkHelper.MachineName && server.ServerAddress == "server1.com"), + }) + && section.WebRouting == Mock.Of(wrSection => wrSection.UmbracoApplicationUrl == (string)null) + && section.ScheduledTasks == Mock.Of(tasksSection => tasksSection.BaseUrl == (string)null)); + + Initialize(settings); + + var appCtx = new ApplicationContext(CacheHelper.CreateDisabledCacheHelper(), + new ProfilingLogger(Mock.Of(), Mock.Of())); + + ConfigurationManager.AppSettings.Set("umbracoUseSSL", "true"); + + ApplicationUrlHelper.TrySetApplicationUrl(appCtx, settings); + + Assert.AreEqual("http://server1.com:80/umbraco", appCtx._umbracoApplicationUrl); + + var registrar = ServerRegistrarResolver.Current.Registrar as IServerRegistrar2; + var role = registrar.GetCurrentServerRole(); + Assert.AreEqual(ServerRole.Slave, role); + } + + [Test] + public void SetApplicationUrlFromDcSettingsSsl3() + { + // set from distributed call settings + // cannot set if not enabled + + var settings = Mock.Of(section => + section.DistributedCall == Mock.Of(callSection => callSection.Enabled == false && callSection.Servers == new IServer[] + { + Mock.Of(server => server.ServerName == "ANOTHERNAME" && server.ServerAddress == "server2.com"), + Mock.Of(server => server.ServerName == NetworkHelper.MachineName && server.ServerAddress == "server1.com"), + }) + && section.WebRouting == Mock.Of(wrSection => wrSection.UmbracoApplicationUrl == (string)null) + && section.ScheduledTasks == Mock.Of(tasksSection => tasksSection.BaseUrl == (string)null)); + + Initialize(settings); + + var appCtx = new ApplicationContext(CacheHelper.CreateDisabledCacheHelper(), + new ProfilingLogger(Mock.Of(), Mock.Of())); + + ConfigurationManager.AppSettings.Set("umbracoUseSSL", "true"); + + ApplicationUrlHelper.TrySetApplicationUrl(appCtx, settings); + + Assert.IsNull(appCtx._umbracoApplicationUrl); + + var registrar = ServerRegistrarResolver.Current.Registrar as IServerRegistrar2; + var role = registrar.GetCurrentServerRole(); + Assert.AreEqual(ServerRole.Single, role); + } + + [Test] + public void ServerRoleSingle() + { + // distributed call settings disabled, single server + + var settings = Mock.Of(section => + section.DistributedCall == Mock.Of(callSection => callSection.Enabled == false && callSection.Servers == Enumerable.Empty()) + && section.WebRouting == Mock.Of(wrSection => wrSection.UmbracoApplicationUrl == (string)null) + && section.ScheduledTasks == Mock.Of(tasksSection => tasksSection.BaseUrl == (string)null)); + + Initialize(settings); + + var registrar = ServerRegistrarResolver.Current.Registrar as IServerRegistrar2; + var role = registrar.GetCurrentServerRole(); + Assert.AreEqual(ServerRole.Single, role); + } + + [Test] + public void ServerRoleUnknown1() + { + // distributed call enabled but missing servers, unknown server + + var settings = Mock.Of(section => + section.DistributedCall == Mock.Of(callSection => callSection.Enabled == true && callSection.Servers == Enumerable.Empty()) + && section.WebRouting == Mock.Of(wrSection => wrSection.UmbracoApplicationUrl == (string)null) + && section.ScheduledTasks == Mock.Of(tasksSection => tasksSection.BaseUrl == (string)null)); + + Initialize(settings); + + var registrar = ServerRegistrarResolver.Current.Registrar as IServerRegistrar2; + var role = registrar.GetCurrentServerRole(); + Assert.AreEqual(ServerRole.Unknown, role); + } + + [Test] + public void ServerRoleUnknown2() + { + // distributed call enabled, cannot find server, assume it's an undeclared slave + + var settings = Mock.Of(section => + section.DistributedCall == Mock.Of(callSection => callSection.Enabled == true && callSection.Servers == new IServer[] + { + Mock.Of(server => server.ServerName == "ANOTHERNAME" && server.ServerAddress == "server2.com"), + }) + && section.WebRouting == Mock.Of(wrSection => wrSection.UmbracoApplicationUrl == (string)null) + && section.ScheduledTasks == Mock.Of(tasksSection => tasksSection.BaseUrl == (string)null)); + + Initialize(settings); + + var registrar = ServerRegistrarResolver.Current.Registrar as IServerRegistrar2; + var role = registrar.GetCurrentServerRole(); + Assert.AreEqual(ServerRole.Slave, role); + } + + [Test] + public void SetApplicationUrlFromStSettingsNoSsl() + { + var settings = Mock.Of(section => + section.DistributedCall == Mock.Of(callSection => callSection.Servers == Enumerable.Empty()) + && section.WebRouting == Mock.Of(wrSection => wrSection.UmbracoApplicationUrl == (string) null) + && section.ScheduledTasks == Mock.Of(tasksSection => tasksSection.BaseUrl == "mycoolhost.com/umbraco")); + + Initialize(settings); + + var appCtx = new ApplicationContext(CacheHelper.CreateDisabledCacheHelper(), + new ProfilingLogger(Mock.Of(), Mock.Of())); + + ConfigurationManager.AppSettings.Set("umbracoUseSSL", "false"); + + ApplicationUrlHelper.TrySetApplicationUrl(appCtx, settings); + + Assert.AreEqual("http://mycoolhost.com/umbraco", appCtx._umbracoApplicationUrl); + } + + [Test] + public void SetApplicationUrlFromStSettingsSsl() + { + var settings = Mock.Of(section => + section.DistributedCall == Mock.Of(callSection => callSection.Servers == Enumerable.Empty()) + && section.WebRouting == Mock.Of(wrSection => wrSection.UmbracoApplicationUrl == (string) null) + && section.ScheduledTasks == Mock.Of(tasksSection => tasksSection.BaseUrl == "mycoolhost.com/umbraco/")); + + Initialize(settings); + + var appCtx = new ApplicationContext(CacheHelper.CreateDisabledCacheHelper(), + new ProfilingLogger(Mock.Of(), Mock.Of())); + + ConfigurationManager.AppSettings.Set("umbracoUseSSL", "true"); + + ApplicationUrlHelper.TrySetApplicationUrl(appCtx, settings); + + Assert.AreEqual("https://mycoolhost.com/umbraco", appCtx._umbracoApplicationUrl); + } + + [Test] + public void SetApplicationUrlFromWrSettingsSsl() + { + var settings = Mock.Of(section => + section.DistributedCall == Mock.Of(callSection => callSection.Servers == Enumerable.Empty()) + && section.WebRouting == Mock.Of(wrSection => wrSection.UmbracoApplicationUrl == "httpx://whatever.com/umbraco/") + && section.ScheduledTasks == Mock.Of(tasksSection => tasksSection.BaseUrl == "mycoolhost.com/umbraco")); + + Initialize(settings); + + var appCtx = new ApplicationContext(CacheHelper.CreateDisabledCacheHelper(), + new ProfilingLogger(Mock.Of(), Mock.Of())); + + ConfigurationManager.AppSettings.Set("umbracoUseSSL", "true"); // does not make a diff here + + ApplicationUrlHelper.TrySetApplicationUrl(appCtx, settings); + + Assert.AreEqual("httpx://whatever.com/umbraco", appCtx._umbracoApplicationUrl); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Tests/ServerEnvironmentHelperTests.cs b/src/Umbraco.Tests/ServerEnvironmentHelperTests.cs deleted file mode 100644 index 7b0bdce8b4..0000000000 --- a/src/Umbraco.Tests/ServerEnvironmentHelperTests.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System.Configuration; -using System.IO; -using System.Linq; -using Moq; -using NUnit.Framework; -using Umbraco.Core; -using Umbraco.Core.Configuration.UmbracoSettings; -using Umbraco.Core.Logging; -using Umbraco.Core.Profiling; -using Umbraco.Core.Sync; -using Umbraco.Tests.TestHelpers; - -namespace Umbraco.Tests -{ - [TestFixture] - public class ServerEnvironmentHelperTests - { - private ILogger _logger; - - // note: in tests, read appContext._umbracoApplicationUrl and not the property, - // because reading the property does run some code, as long as the field is null. - - [TestFixtureSetUp] - public void InitializeFixture() - { - _logger = new Logger(new FileInfo(TestHelper.MapPathForTest("~/unit-test-log4net.config"))); - } - - [Test] - public void SetApplicationUrlWhenNoSettings() - { - var appCtx = new ApplicationContext( - CacheHelper.CreateDisabledCacheHelper(), - new ProfilingLogger(Mock.Of(), Mock.Of())) - { - UmbracoApplicationUrl = null // NOT set - }; - - - - ConfigurationManager.AppSettings.Set("umbracoUseSSL", "true"); // does not make a diff here - - ServerEnvironmentHelper.TrySetApplicationUrlFromSettings(appCtx, _logger, - Mock.Of( - section => - section.DistributedCall == Mock.Of(callSection => callSection.Servers == Enumerable.Empty()) - && section.WebRouting == Mock.Of(wrSection => wrSection.UmbracoApplicationUrl == (string) null) - && section.ScheduledTasks == Mock.Of())); - - - // still NOT set - Assert.IsNull(appCtx._umbracoApplicationUrl); - } - - [Test] - public void SetApplicationUrlFromDcSettingsNoSsl() - { - var appCtx = new ApplicationContext( - CacheHelper.CreateDisabledCacheHelper(), - new ProfilingLogger(Mock.Of(), Mock.Of())); - - ConfigurationManager.AppSettings.Set("umbracoUseSSL", "false"); - - ServerEnvironmentHelper.TrySetApplicationUrlFromSettings(appCtx, _logger, - Mock.Of( - section => - section.DistributedCall == Mock.Of(callSection => callSection.Servers == Enumerable.Empty()) - && section.WebRouting == Mock.Of(wrSection => wrSection.UmbracoApplicationUrl == (string) null) - && section.ScheduledTasks == Mock.Of(tasksSection => tasksSection.BaseUrl == "mycoolhost.com/hello/world/"))); - - - Assert.AreEqual("http://mycoolhost.com/hello/world", appCtx._umbracoApplicationUrl); - } - - [Test] - public void SetApplicationUrlFromDcSettingsSsl() - { - var appCtx = new ApplicationContext( - CacheHelper.CreateDisabledCacheHelper(), - new ProfilingLogger(Mock.Of(), Mock.Of())); - - ConfigurationManager.AppSettings.Set("umbracoUseSSL", "true"); - - ServerEnvironmentHelper.TrySetApplicationUrlFromSettings(appCtx, _logger, - Mock.Of( - section => - section.DistributedCall == Mock.Of(callSection => callSection.Servers == Enumerable.Empty()) - && section.WebRouting == Mock.Of(wrSection => wrSection.UmbracoApplicationUrl == (string) null) - && section.ScheduledTasks == Mock.Of(tasksSection => tasksSection.BaseUrl == "mycoolhost.com/hello/world"))); - - - Assert.AreEqual("https://mycoolhost.com/hello/world", appCtx._umbracoApplicationUrl); - } - - [Test] - public void SetApplicationUrlFromWrSettingsSsl() - { - var appCtx = new ApplicationContext( - CacheHelper.CreateDisabledCacheHelper(), - new ProfilingLogger(Mock.Of(), Mock.Of())); - - ConfigurationManager.AppSettings.Set("umbracoUseSSL", "true"); // does not make a diff here - - ServerEnvironmentHelper.TrySetApplicationUrlFromSettings(appCtx, _logger, - Mock.Of( - section => - section.DistributedCall == Mock.Of(callSection => callSection.Servers == Enumerable.Empty()) - && section.WebRouting == Mock.Of(wrSection => wrSection.UmbracoApplicationUrl == "httpx://whatever.com/hello/world/") - && section.ScheduledTasks == Mock.Of(tasksSection => tasksSection.BaseUrl == "mycoolhost.com/hello/world"))); - - - Assert.AreEqual("httpx://whatever.com/hello/world", appCtx._umbracoApplicationUrl); - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index a985a2139d..05a57928e0 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -342,7 +342,7 @@ - + diff --git a/src/Umbraco.Web/Scheduling/LogScrubber.cs b/src/Umbraco.Web/Scheduling/LogScrubber.cs index 66ba966823..b3a5f303e3 100644 --- a/src/Umbraco.Web/Scheduling/LogScrubber.cs +++ b/src/Umbraco.Web/Scheduling/LogScrubber.cs @@ -60,10 +60,14 @@ namespace Umbraco.Web.Scheduling { if (_appContext == null) return true; // repeat... - if (ServerEnvironmentHelper.GetStatus(_settings) == CurrentServerEnvironmentStatus.Slave) + switch (_appContext.GetCurrentServerRole()) { - LogHelper.Debug("Does not run on slave servers."); - return false; // do NOT repeat, server status comes from config and will NOT change + case ServerRole.Slave: + LogHelper.Debug("Does not run on slave servers."); + return true; // DO repeat, server role can change + case ServerRole.Unknown: + LogHelper.Debug("Does not run on servers with unknown role."); + return true; // DO repeat, server role can change } // ensure we do not run if not main domain, but do NOT lock it diff --git a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs index 10d4239a27..0f7e3f0183 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs @@ -32,10 +32,14 @@ namespace Umbraco.Web.Scheduling { if (_appContext == null) return true; // repeat... - if (ServerEnvironmentHelper.GetStatus(_settings) == CurrentServerEnvironmentStatus.Slave) + switch (_appContext.GetCurrentServerRole()) { - LogHelper.Debug("Does not run on slave servers."); - return false; // do NOT repeat, server status comes from config and will NOT change + case ServerRole.Slave: + LogHelper.Debug("Does not run on slave servers."); + return true; // DO repeat, server role can change + case ServerRole.Unknown: + LogHelper.Debug("Does not run on servers with unknown role."); + return true; // DO repeat, server role can change } // ensure we do not run if not main domain, but do NOT lock it diff --git a/src/Umbraco.Web/Scheduling/ScheduledTasks.cs b/src/Umbraco.Web/Scheduling/ScheduledTasks.cs index 8bd5e9ebc0..37d7a190f7 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledTasks.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledTasks.cs @@ -90,10 +90,14 @@ namespace Umbraco.Web.Scheduling { if (_appContext == null) return true; // repeat... - if (ServerEnvironmentHelper.GetStatus(_settings) == CurrentServerEnvironmentStatus.Slave) + switch (_appContext.GetCurrentServerRole()) { - LogHelper.Debug("Does not run on slave servers."); - return false; // do NOT repeat, server status comes from config and will NOT change + case ServerRole.Slave: + LogHelper.Debug("Does not run on slave servers."); + return true; // DO repeat, server role can change + case ServerRole.Unknown: + LogHelper.Debug("Does not run on servers with unknown role."); + return true; // DO repeat, server role can change } // ensure we do not run if not main domain, but do NOT lock it diff --git a/src/Umbraco.Web/Strategies/ServerRegistrationEventHandler.cs b/src/Umbraco.Web/Strategies/ServerRegistrationEventHandler.cs index 6d8faa782f..b23c0870bc 100644 --- a/src/Umbraco.Web/Strategies/ServerRegistrationEventHandler.cs +++ b/src/Umbraco.Web/Strategies/ServerRegistrationEventHandler.cs @@ -3,6 +3,7 @@ using System.Web; using Newtonsoft.Json; using Umbraco.Core; using Umbraco.Core.Logging; +using Umbraco.Core.Services; using Umbraco.Core.Sync; using Umbraco.Web.Routing; @@ -21,18 +22,23 @@ namespace Umbraco.Web.Strategies /// public sealed class ServerRegistrationEventHandler : ApplicationEventHandler { - private static DateTime _lastUpdated = DateTime.MinValue; + private readonly object _locko = new object(); + private DatabaseServerRegistrar _registrar; + private DateTime _lastUpdated = DateTime.MinValue; // bind to events protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) { + _registrar = ServerRegistrarResolver.Current.Registrar as DatabaseServerRegistrar; + // only for the DatabaseServerRegistrar - if (ServerRegistrarResolver.Current.Registrar is DatabaseServerRegistrar) - UmbracoModule.RouteAttempt += UmbracoModuleRouteAttempt; + if (_registrar == null) return; + + UmbracoModule.RouteAttempt += UmbracoModuleRouteAttempt; } // handles route attempts. - private static void UmbracoModuleRouteAttempt(object sender, RoutableAttemptEventArgs e) + private void UmbracoModuleRouteAttempt(object sender, RoutableAttemptEventArgs e) { if (e.HttpContext.Request == null || e.HttpContext.Request.Url == null) return; @@ -60,31 +66,26 @@ namespace Umbraco.Web.Strategies } // register current server (throttled). - private static void RegisterServer(UmbracoRequestEventArgs e) + private void RegisterServer(UmbracoRequestEventArgs e) { - var reg = (DatabaseServerRegistrar) ServerRegistrarResolver.Current.Registrar; - var options = reg.Options; - var secondsSinceLastUpdate = DateTime.Now.Subtract(_lastUpdated).TotalSeconds; - if (secondsSinceLastUpdate < options.ThrottleSeconds) return; + lock (_locko) // ensure we trigger only once + { + var secondsSinceLastUpdate = DateTime.Now.Subtract(_lastUpdated).TotalSeconds; + if (secondsSinceLastUpdate < _registrar.Options.ThrottleSeconds) return; + _lastUpdated = DateTime.Now; + } - _lastUpdated = DateTime.Now; + var svc = e.UmbracoContext.Application.Services.ServerRegistrationService as ServerRegistrationService; - var url = e.HttpContext.Request.Url; - var svc = e.UmbracoContext.Application.Services.ServerRegistrationService; + // because + // - ApplicationContext.UmbracoApplicationUrl is initialized by UmbracoModule in BeginRequest + // - RegisterServer is called on UmbracoModule.RouteAttempt which is triggered in ProcessRequest + // we are safe, UmbracoApplicationUrl has been initialized + var serverAddress = e.UmbracoContext.Application.UmbracoApplicationUrl; try { - if (url == null) - throw new Exception("Request.Url is null."); - - var serverAddress = url.GetLeftPart(UriPartial.Authority); - var serverIdentity = JsonConvert.SerializeObject(new - { - machineName = NetworkHelper.MachineName, - appDomainAppId = HttpRuntime.AppDomainAppId - }); - - svc.TouchServer(serverAddress, serverIdentity, options.StaleServerTimeout); + svc.TouchServer(serverAddress, svc.CurrentServerIdentity, _registrar.Options.StaleServerTimeout); } catch (Exception ex) { diff --git a/src/Umbraco.Web/UmbracoModule.cs b/src/Umbraco.Web/UmbracoModule.cs index 54d106574a..02bb83bd32 100644 --- a/src/Umbraco.Web/UmbracoModule.cs +++ b/src/Umbraco.Web/UmbracoModule.cs @@ -20,6 +20,7 @@ using Umbraco.Web.Editors; using Umbraco.Web.Routing; using Umbraco.Web.Security; using umbraco; +using Umbraco.Core.Sync; using GlobalSettings = Umbraco.Core.Configuration.GlobalSettings; using ObjectExtensions = Umbraco.Core.ObjectExtensions; using RenderingEngine = Umbraco.Core.RenderingEngine; @@ -36,36 +37,6 @@ namespace Umbraco.Web { #region HttpModule event handlers - private static void EnsureApplicationUrl(HttpRequestBase request) - { - var appctx = ApplicationContext.Current; - - // already initialized = ok - // note that getting ApplicationUrl will ALSO try the various settings - if (appctx.UmbracoApplicationUrl.IsNullOrWhiteSpace() == false) return; - - // so if we reach that point, nothing was configured - // use the current request as application url - - // if (HTTP and SSL not required) or (HTTPS and SSL required), - // use ports from request - // otherwise, - // if non-standard ports used, - // user may need to set umbracoApplicationUrl manually per - // http://our.umbraco.org/documentation/Using-Umbraco/Config-files/umbracoSettings/#ScheduledTasks - var port = (request.IsSecureConnection == false && GlobalSettings.UseSSL == false) - || (request.IsSecureConnection && GlobalSettings.UseSSL) - ? ":" + request.ServerVariables["SERVER_PORT"] - : ""; - - var ssl = GlobalSettings.UseSSL ? "s" : ""; // force, whatever the first request - var url = "http" + ssl + "://" + request.ServerVariables["SERVER_NAME"] + port + IOHelper.ResolveUrl(SystemDirectories.Umbraco); - - appctx.UmbracoApplicationUrl = UriUtility.TrimPathEndSlash(url); - LogHelper.Info("ApplicationUrl: " + appctx.UmbracoApplicationUrl + " (UmbracoModule request)"); - } - - /// /// Begins to process a request. /// @@ -73,7 +44,7 @@ namespace Umbraco.Web static void BeginRequest(HttpContextBase httpContext) { // ensure application url is initialized - EnsureApplicationUrl(httpContext.Request); + ApplicationUrlHelper.EnsureApplicationUrl(ApplicationContext.Current, httpContext.Request); // do not process if client-side request if (httpContext.Request.Url.IsClientSideRequest())