diff --git a/src/Umbraco.Core/CoreBootManager.cs b/src/Umbraco.Core/CoreBootManager.cs index 4aeee2881d..294ad69a87 100644 --- a/src/Umbraco.Core/CoreBootManager.cs +++ b/src/Umbraco.Core/CoreBootManager.cs @@ -320,7 +320,7 @@ namespace Umbraco.Core //supplying a username/password, this will automatically disable distributed calls // .. we'll override this in the WebBootManager ServerMessengerResolver.Current = new ServerMessengerResolver( - new DefaultServerMessenger()); + new WebServiceServerMessenger()); MappingResolver.Current = new MappingResolver( ServiceProvider, LoggerResolver.Current.Logger, diff --git a/src/Umbraco.Core/Logging/AppDomainTokenFormatter.cs b/src/Umbraco.Core/Logging/AppDomainTokenFormatter.cs new file mode 100644 index 0000000000..0abddc63e3 --- /dev/null +++ b/src/Umbraco.Core/Logging/AppDomainTokenFormatter.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Web; + +namespace Umbraco.Core.Logging +{ + /// + /// Allows for outputting a normalized appdomainappid token in a log format + /// + public sealed class AppDomainTokenConverter : log4net.Util.PatternConverter + { + protected override void Convert(TextWriter writer, object state) + { + writer.Write(HttpRuntime.AppDomainAppId.ReplaceNonAlphanumericChars(string.Empty)); + } + } +} diff --git a/src/Umbraco.Core/Models/Rdbms/CacheInstructionDto.cs b/src/Umbraco.Core/Models/Rdbms/CacheInstructionDto.cs new file mode 100644 index 0000000000..c24004b7dc --- /dev/null +++ b/src/Umbraco.Core/Models/Rdbms/CacheInstructionDto.cs @@ -0,0 +1,31 @@ +using System; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.DatabaseAnnotations; + +namespace Umbraco.Core.Models.Rdbms +{ + [TableName("umbracoCacheInstruction")] + [PrimaryKey("id")] + [ExplicitColumns] + internal class CacheInstructionDto + { + [Column("id")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [PrimaryKeyColumn(AutoIncrement = true, Name = "PK_umbracoCacheInstruction")] + public int Id { get; set; } + + [Column("utcStamp")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public DateTime UtcStamp { get; set; } + + [Column("jsonInstruction")] + [SpecialDbType(SpecialDbTypes.NTEXT)] + [NullSetting(NullSetting = NullSettings.NotNull)] + public string Instructions { get; set; } + + [Column("originated")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Length(500)] + public string OriginIdentity { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Rdbms/ServerRegistrationDto.cs b/src/Umbraco.Core/Models/Rdbms/ServerRegistrationDto.cs index 9833d9ab74..b7bdf265ce 100644 --- a/src/Umbraco.Core/Models/Rdbms/ServerRegistrationDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/ServerRegistrationDto.cs @@ -15,22 +15,19 @@ namespace Umbraco.Core.Models.Rdbms [Column("address")] [Length(500)] - public string Address { get; set; } + public string ServerAddress { get; set; } - /// - /// A unique column in the database, a computer name must always be unique! - /// [Column("computerName")] [Length(255)] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_computerName")] - public string ComputerName { get; set; } + [Index(IndexTypes.UniqueNonClustered, Name = "IX_computerName")] // server identity is unique + public string ServerIdentity { get; set; } [Column("registeredDate")] [Constraint(Default = "getdate()")] public DateTime DateRegistered { get; set; } [Column("lastNotifiedDate")] - public DateTime LastNotified { get; set; } + public DateTime DateAccessed { get; set; } [Column("isActive")] [Index(IndexTypes.NonClustered)] diff --git a/src/Umbraco.Core/Models/ServerRegistration.cs b/src/Umbraco.Core/Models/ServerRegistration.cs index 7f43f5dfd2..900d6deb94 100644 --- a/src/Umbraco.Core/Models/ServerRegistration.cs +++ b/src/Umbraco.Core/Models/ServerRegistration.cs @@ -2,61 +2,67 @@ using System.Globalization; using System.Reflection; using Umbraco.Core.Models.EntityBase; -using Umbraco.Core.Persistence.Mappers; using Umbraco.Core.Sync; namespace Umbraco.Core.Models { - internal class ServerRegistration : Entity, IServerAddress, IAggregateRoot + /// + /// Represents a registered server in a multiple-servers environment. + /// + public class ServerRegistration : Entity, IServerAddress, IAggregateRoot { private string _serverAddress; - private string _computerName; + private string _serverIdentity; private bool _isActive; private static readonly PropertyInfo ServerAddressSelector = ExpressionHelper.GetPropertyInfo(x => x.ServerAddress); - private static readonly PropertyInfo ComputerNameSelector = ExpressionHelper.GetPropertyInfo(x => x.ComputerName); + private static readonly PropertyInfo ServerIdentitySelector = ExpressionHelper.GetPropertyInfo(x => x.ServerIdentity); private static readonly PropertyInfo IsActiveSelector = ExpressionHelper.GetPropertyInfo(x => x.IsActive); + /// + /// Initialiazes a new instance of the class. + /// public ServerRegistration() - { - - } + { } /// - /// Creates an item with pre-filled properties + /// Initialiazes a new instance of the class. /// - /// - /// - /// - /// - /// - /// - public ServerRegistration(int id, string serverAddress, string computerName, DateTime createDate, DateTime updateDate, bool isActive) + /// The unique id of the server registration. + /// The server url. + /// The unique server identity. + /// 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) { - UpdateDate = updateDate; - CreateDate = createDate; - Key = Id.ToString(CultureInfo.InvariantCulture).EncodeAsGuid(); + UpdateDate = accessed; + CreateDate = registered; + Key = id.ToString(CultureInfo.InvariantCulture).EncodeAsGuid(); Id = id; ServerAddress = serverAddress; - ComputerName = computerName; + ServerIdentity = serverIdentity; IsActive = isActive; } /// - /// Creates a new instance for persisting a new item + /// Initialiazes a new instance of the class. /// - /// - /// - /// - public ServerRegistration(string serverAddress, string computerName, DateTime createDate) + /// The server url. + /// The unique server identity. + /// The date and time the registration was created. + public ServerRegistration(string serverAddress, string serverIdentity, DateTime registered) { - CreateDate = createDate; - UpdateDate = createDate; + CreateDate = registered; + UpdateDate = registered; Key = 0.ToString(CultureInfo.InvariantCulture).EncodeAsGuid(); ServerAddress = serverAddress; - ComputerName = computerName; + ServerIdentity = serverIdentity; } + /// + /// Gets or sets the server url. + /// public string ServerAddress { get { return _serverAddress; } @@ -70,19 +76,25 @@ namespace Umbraco.Core.Models } } - public string ComputerName + /// + /// Gets or sets the server unique identity. + /// + public string ServerIdentity { - get { return _computerName; } + get { return _serverIdentity; } set { SetPropertyValueAndDetectChanges(o => { - _computerName = value; - return _computerName; - }, _computerName, ComputerNameSelector); + _serverIdentity = value; + return _serverIdentity; + }, _serverIdentity, ServerIdentitySelector); } } + /// + /// Gets or sets a value indicating whether the server is active. + /// public bool IsActive { get { return _isActive; } @@ -96,9 +108,23 @@ namespace Umbraco.Core.Models } } + /// + /// Gets the date and time the registration was created. + /// + public DateTime Registered { get { return CreateDate; } set { CreateDate = value; }} + + /// + /// Gets the date and time the registration was last accessed. + /// + public DateTime Accessed { get { return UpdateDate; } set { UpdateDate = value; }} + + /// + /// Converts the value of this instance to its equivalent string representation. + /// + /// public override string ToString() { - return "(" + ServerAddress + ", " + ComputerName + ", IsActive = " + IsActive + ")"; + return string.Format("{{\"{0}\", \"{1}\", {2}active}}", ServerAddress, ServerIdentity, IsActive ? "" : "!"); } } } \ 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 e13b24e1e1..aa0ed25ccd 100644 --- a/src/Umbraco.Core/Persistence/Factories/ServerRegistrationFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/ServerRegistrationFactory.cs @@ -1,5 +1,4 @@ -using System.Globalization; -using Umbraco.Core.Models; +using Umbraco.Core.Models; using Umbraco.Core.Models.Rdbms; namespace Umbraco.Core.Persistence.Factories @@ -10,7 +9,7 @@ namespace Umbraco.Core.Persistence.Factories public ServerRegistration BuildEntity(ServerRegistrationDto dto) { - var model = new ServerRegistration(dto.Id, dto.Address, dto.ComputerName, dto.DateRegistered, dto.LastNotified, dto.IsActive); + var model = new ServerRegistration(dto.Id, dto.ServerAddress, dto.ServerIdentity, dto.DateRegistered, dto.DateAccessed, dto.IsActive); //on initial construction we don't want to have dirty properties tracked // http://issues.umbraco.org/issue/U4-1946 model.ResetDirtyProperties(false); @@ -19,16 +18,17 @@ namespace Umbraco.Core.Persistence.Factories public ServerRegistrationDto BuildDto(ServerRegistration entity) { - var dto = new ServerRegistrationDto() + var dto = new ServerRegistrationDto { - Address = entity.ServerAddress, + ServerAddress = entity.ServerAddress, DateRegistered = entity.CreateDate, IsActive = entity.IsActive, - LastNotified = entity.UpdateDate, - ComputerName = entity.ComputerName + DateAccessed = entity.UpdateDate, + ServerIdentity = entity.ServerIdentity }; + if (entity.HasIdentity) - dto.Id = int.Parse(entity.Id.ToString(CultureInfo.InvariantCulture)); + dto.Id = entity.Id; return dto; } diff --git a/src/Umbraco.Core/Persistence/Mappers/ServerRegistrationMapper.cs b/src/Umbraco.Core/Persistence/Mappers/ServerRegistrationMapper.cs index 24b0632f8a..40a18adc59 100644 --- a/src/Umbraco.Core/Persistence/Mappers/ServerRegistrationMapper.cs +++ b/src/Umbraco.Core/Persistence/Mappers/ServerRegistrationMapper.cs @@ -7,7 +7,7 @@ using Umbraco.Core.Models.Rdbms; namespace Umbraco.Core.Persistence.Mappers { [MapperFor(typeof(ServerRegistration))] - public sealed class ServerRegistrationMapper : BaseMapper + internal sealed class ServerRegistrationMapper : BaseMapper { private static readonly ConcurrentDictionary PropertyInfoCacheInstance = new ConcurrentDictionary(); @@ -29,10 +29,10 @@ namespace Umbraco.Core.Persistence.Mappers { CacheMap(src => src.Id, dto => dto.Id); CacheMap(src => src.IsActive, dto => dto.IsActive); - CacheMap(src => src.ServerAddress, dto => dto.Address); + CacheMap(src => src.ServerAddress, dto => dto.ServerAddress); CacheMap(src => src.CreateDate, dto => dto.DateRegistered); - CacheMap(src => src.UpdateDate, dto => dto.LastNotified); - CacheMap(src => src.ComputerName, dto => dto.ComputerName); + CacheMap(src => src.UpdateDate, dto => dto.DateAccessed); + CacheMap(src => src.ServerIdentity, dto => dto.ServerIdentity); } #endregion diff --git a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs index b77e0843ea..76689948c5 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs @@ -79,9 +79,9 @@ namespace Umbraco.Core.Persistence.Migrations.Initial {38, typeof (User2NodeNotifyDto)}, {39, typeof (User2NodePermissionDto)}, {40, typeof (ServerRegistrationDto)}, - {41, typeof (AccessDto)}, - {42, typeof (AccessRuleDto)} + {42, typeof (AccessRuleDto)}, + {43, typeof(CacheInstructionDto)} }; #endregion diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/CreateCacheInstructionTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/CreateCacheInstructionTable.cs new file mode 100644 index 0000000000..66391f9fe5 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/CreateCacheInstructionTable.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core.Configuration; +using Umbraco.Core.Persistence.DatabaseAnnotations; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZero +{ + [Migration("7.3.0", 1, GlobalSettings.UmbracoMigrationName)] + public class CreateCacheInstructionTable : MigrationBase + { + public override void Up() + { + var textType = SqlSyntaxContext.SqlSyntaxProvider.GetSpecialDbType(SpecialDbTypes.NTEXT); + + Create.Table("umbracoCacheInstruction") + .WithColumn("id").AsInt32().Identity().NotNullable() + .WithColumn("utcStamp").AsDateTime().NotNullable() + .WithColumn("jsonInstruction").AsCustom(textType).NotNullable(); + + Create.PrimaryKey("PK_umbracoCacheInstruction") + .OnTable("umbracoCacheInstruction") + .Column("id"); + } + + public override void Down() + { + Delete.PrimaryKey("PK_umbracoCacheInstruction").FromTable("cmsContentType2ContentType"); + Delete.Table("cmsContentType2ContentType"); + } + } +} diff --git a/src/Umbraco.Core/Persistence/Repositories/ServerRegistrationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ServerRegistrationRepository.cs index 020ae2e9cf..1ef87357ea 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ServerRegistrationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ServerRegistrationRepository.cs @@ -125,5 +125,12 @@ namespace Umbraco.Core.Persistence.Repositories entity.ResetDirtyProperties(); } + public void DeactiveStaleServers(TimeSpan staleTimeout) + { + var timeoutDate = DateTime.UtcNow.Subtract(staleTimeout); + + Database.Update("SET isActive=0 WHERE lastNotifiedDate < @timeoutDate", new {timeoutDate = timeoutDate}); + } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/ServerRegistrationService.cs b/src/Umbraco.Core/Services/ServerRegistrationService.cs index f52c05768f..157e7b795d 100644 --- a/src/Umbraco.Core/Services/ServerRegistrationService.cs +++ b/src/Umbraco.Core/Services/ServerRegistrationService.cs @@ -11,63 +11,66 @@ namespace Umbraco.Core.Services { /// - /// Service to manage server registrations in the database + /// Manages server registrations in the database. /// - internal class ServerRegistrationService : RepositoryService + public sealed class ServerRegistrationService : RepositoryService { - - public ServerRegistrationService(IDatabaseUnitOfWorkProvider provider, RepositoryFactory repositoryFactory, ILogger logger) - : base(provider, repositoryFactory, logger) - { - } + /// + /// Initializes a new instance of the class. + /// + /// A UnitOfWork provider. + /// A repository factory. + /// A logger. + public ServerRegistrationService(IDatabaseUnitOfWorkProvider uowProvider, RepositoryFactory repositoryFactory, ILogger logger) + : base(uowProvider, repositoryFactory, logger) + { } /// - /// Called to 'call home' to ensure the current server has an active record + /// Touches a server to mark it as active; deactivate stale servers. /// - /// - public void EnsureActive(string address) + /// The server url. + /// The server unique identity. + /// 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)) { - //NOTE: we cannot use Environment.MachineName as this does not work in medium trust - // found this out in CDF a while back: http://clientdependency.codeplex.com/workitem/13191 - - var computerName = System.Net.Dns.GetHostName(); - var query = Query.Builder.Where(x => x.ComputerName.ToUpper() == computerName.ToUpper()); - var found = repo.GetByQuery(query).ToArray(); - ServerRegistration server; - if (found.Any()) + var query = Query.Builder.Where(x => x.ServerIdentity.ToUpper() == serverIdentity.ToUpper()); + var server = repo.GetByQuery(query).FirstOrDefault(); + if (server == null) { - server = found.First(); - server.ServerAddress = address; //This 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 = new ServerRegistration(serverAddress, serverIdentity, DateTime.UtcNow) + { + IsActive = true + }; } else { - server = new ServerRegistration(address, computerName, DateTime.UtcNow); + 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; } repo.AddOrUpdate(server); uow.Commit(); + + repo.DeactiveStaleServers(staleTimeout); } } /// - /// Deactivates a server by name + /// Deactivates a server. /// - /// - public void DeactiveServer(string computerName) + /// The server unique identity. + public void DeactiveServer(string serverIdentity) { var uow = UowProvider.GetUnitOfWork(); using (var repo = RepositoryFactory.CreateServerRegistrationRepository(uow)) { - var query = Query.Builder.Where(x => x.ComputerName.ToUpper() == computerName.ToUpper()); - var found = repo.GetByQuery(query).ToArray(); - if (found.Any()) + var query = Query.Builder.Where(x => x.ServerIdentity.ToUpper() == serverIdentity.ToUpper()); + var server = repo.GetByQuery(query).FirstOrDefault(); + if (server != null) { - var server = found.First(); server.IsActive = false; repo.AddOrUpdate(server); uow.Commit(); @@ -76,7 +79,20 @@ namespace Umbraco.Core.Services } /// - /// Return all active servers + /// Deactivates stale servers. + /// + /// 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); + } + } + + /// + /// Return all active servers. /// /// public IEnumerable GetActiveServers() diff --git a/src/Umbraco.Core/Services/ServiceContext.cs b/src/Umbraco.Core/Services/ServiceContext.cs index f1eb873db7..255f6e457f 100644 --- a/src/Umbraco.Core/Services/ServiceContext.cs +++ b/src/Umbraco.Core/Services/ServiceContext.cs @@ -274,7 +274,7 @@ namespace Umbraco.Core.Services /// /// Gets the /// - internal ServerRegistrationService ServerRegistrationService + public ServerRegistrationService ServerRegistrationService { get { return _serverRegistrationService.Value; } } diff --git a/src/Umbraco.Core/StringExtensions.cs b/src/Umbraco.Core/StringExtensions.cs index 86a2f9c36e..eb7c0f4975 100644 --- a/src/Umbraco.Core/StringExtensions.cs +++ b/src/Umbraco.Core/StringExtensions.cs @@ -114,13 +114,11 @@ namespace Umbraco.Core internal static string ReplaceNonAlphanumericChars(this string input, char replacement) { - //any character that is not alphanumeric, convert to a hyphen - var mName = input; - foreach (var c in mName.ToCharArray().Where(c => !char.IsLetterOrDigit(c))) - { - mName = mName.Replace(c, replacement); - } - return mName; + var inputArray = input.ToCharArray(); + var outputArray = new char[input.Length]; + for (var i = 0; i < inputArray.Length; i++) + outputArray[i] = char.IsLetterOrDigit(inputArray[i]) ? inputArray[i] : replacement; + return new string(outputArray); } /// diff --git a/src/Umbraco.Core/Sync/BatchedDatabaseServerMessenger.cs b/src/Umbraco.Core/Sync/BatchedDatabaseServerMessenger.cs new file mode 100644 index 0000000000..5c439377f3 --- /dev/null +++ b/src/Umbraco.Core/Sync/BatchedDatabaseServerMessenger.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using Umbraco.Core.Models.Rdbms; +using umbraco.interfaces; + +namespace Umbraco.Core.Sync +{ + // abstract because it needs to be inherited by a class that will + // - trigger FlushBatch() when appropriate + // - trigger Boot() when appropriate + // - trigger Sync() when appropriate + // + public abstract class BatchedDatabaseServerMessenger : DatabaseServerMessenger + { + private readonly Func> _getBatch; + + protected BatchedDatabaseServerMessenger(ApplicationContext appContext, bool enableDistCalls, DatabaseServerMessengerOptions options, + Func> getBatch) + : base(appContext, enableDistCalls, options) + { + if (getBatch == null) + throw new ArgumentNullException("getBatch"); + + _getBatch = getBatch; + } + + public void FlushBatch() + { + var batch = _getBatch(false); + if (batch == null) return; + + var instructions = batch.SelectMany(x => x.Instructions).ToArray(); + batch.Clear(); + if (instructions.Length == 0) return; + + var dto = new CacheInstructionDto + { + UtcStamp = DateTime.UtcNow, + Instructions = JsonConvert.SerializeObject(instructions, Formatting.None), + OriginIdentity = GetLocalIdentity() + }; + + ApplicationContext.DatabaseContext.Database.Insert(dto); + } + + protected override void DeliverRemote(IEnumerable servers, ICacheRefresher refresher, MessageType messageType, IEnumerable ids = null, string json = null) + { + var idsA = ids == null ? null : ids.ToArray(); + + Type arrayType; + if (GetArrayType(idsA, out arrayType) == false) + throw new ArgumentException("All items must be of the same type, either int or Guid.", "ids"); + + BatchMessage(servers, refresher, messageType, idsA, arrayType, json); + } + + protected void BatchMessage( + IEnumerable servers, + ICacheRefresher refresher, + MessageType messageType, + IEnumerable ids = null, + Type idType = null, + string json = null) + { + var batch = _getBatch(true); + if (batch == null) + throw new Exception("Failed to get a batch."); + + batch.Add(new RefreshInstructionEnvelope(servers, refresher, + RefreshInstruction.GetInstructions(refresher, messageType, ids, idType, json))); + } + } +} diff --git a/src/Umbraco.Core/Sync/BatchedWebServiceServerMessenger.cs b/src/Umbraco.Core/Sync/BatchedWebServiceServerMessenger.cs new file mode 100644 index 0000000000..57dd273f2a --- /dev/null +++ b/src/Umbraco.Core/Sync/BatchedWebServiceServerMessenger.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using umbraco.interfaces; + +namespace Umbraco.Core.Sync +{ + // abstract because it needs to be inherited by a class that will + // - implement ProcessBatch() + // - trigger FlushBatch() when appropriate + // + internal abstract class BatchedWebServiceServerMessenger : WebServiceServerMessenger + { + private readonly Func> _getBatch; + + internal BatchedWebServiceServerMessenger(Func> getBatch) + { + if (getBatch == null) + throw new ArgumentNullException("getBatch"); + + _getBatch = getBatch; + } + + internal BatchedWebServiceServerMessenger(string login, string password, Func> getBatch) + : base(login, password) + { + if (getBatch == null) + throw new ArgumentNullException("getBatch"); + + _getBatch = getBatch; + } + + internal BatchedWebServiceServerMessenger(string login, string password, bool useDistributedCalls, Func> getBatch) + : base(login, password, useDistributedCalls) + { + if (getBatch == null) + throw new ArgumentNullException("getBatch"); + + _getBatch = getBatch; + } + + protected BatchedWebServiceServerMessenger(Func> getLoginAndPassword, Func> getBatch) + : base(getLoginAndPassword) + { + if (getBatch == null) + throw new ArgumentNullException("getBatch"); + + _getBatch = getBatch; + } + + protected void FlushBatch() + { + var batch = _getBatch(false); + if (batch == null) return; + + var batcha = batch.ToArray(); + batch.Clear(); + if (batcha.Length == 0) return; + + ProcessBatch(batcha); + } + + // needs to be overriden to actually do something + protected abstract void ProcessBatch(RefreshInstructionEnvelope[] batch); + + protected override void DeliverRemote(IEnumerable servers, ICacheRefresher refresher, MessageType messageType, IEnumerable ids = null, string json = null) + { + var idsA = ids == null ? null : ids.ToArray(); + + Type arrayType; + if (GetArrayType(idsA, out arrayType) == false) + throw new ArgumentException("All items must be of the same type, either int or Guid.", "ids"); + + BatchMessage(servers, refresher, messageType, idsA, arrayType, json); + } + + protected void BatchMessage( + IEnumerable servers, + ICacheRefresher refresher, + MessageType messageType, + IEnumerable ids = null, + Type idType = null, + string json = null) + { + var batch = _getBatch(true); + if (batch == null) + throw new Exception("Failed to get a batch."); + + batch.Add(new RefreshInstructionEnvelope(servers, refresher, + RefreshInstruction.GetInstructions(refresher, messageType, ids, idType, json))); + } + } +} diff --git a/src/Umbraco.Core/Sync/ConfigServerAddress.cs b/src/Umbraco.Core/Sync/ConfigServerAddress.cs index 1bfa7a305e..431dcd9573 100644 --- a/src/Umbraco.Core/Sync/ConfigServerAddress.cs +++ b/src/Umbraco.Core/Sync/ConfigServerAddress.cs @@ -1,16 +1,14 @@ -using System.Xml; -using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; namespace Umbraco.Core.Sync { /// - /// A server registration based on the legacy umbraco xml configuration in umbracoSettings + /// Provides the address of a server based on the Xml configuration. /// internal class ConfigServerAddress : IServerAddress { - public ConfigServerAddress(IServer n) { var webServicesUrl = IOHelper.ResolveUrl(SystemDirectories.WebServices); @@ -29,7 +27,6 @@ namespace Umbraco.Core.Sync public override string ToString() { return ServerAddress; - } - + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Sync/ConfigServerRegistrar.cs b/src/Umbraco.Core/Sync/ConfigServerRegistrar.cs index 69f54fcab7..06cf03f7ce 100644 --- a/src/Umbraco.Core/Sync/ConfigServerRegistrar.cs +++ b/src/Umbraco.Core/Sync/ConfigServerRegistrar.cs @@ -1,53 +1,35 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Xml; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; -using Umbraco.Core.Models; namespace Umbraco.Core.Sync { /// - /// A registrar that uses the legacy xml configuration in umbracoSettings to get a list of defined server nodes + /// 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 { - private readonly IEnumerable _servers; + private readonly List _addresses; public ConfigServerRegistrar() : this(UmbracoConfig.For.UmbracoSettings().DistributedCall.Servers) - { - - } + { } internal ConfigServerRegistrar(IEnumerable servers) { - _servers = servers; + _addresses = servers == null + ? new List() + : servers + .Select(x => new ConfigServerAddress(x)) + .Cast() + .ToList(); } - private List _addresses; - public IEnumerable Registrations { - get - { - if (_addresses == null) - { - _addresses = new List(); - - if (_servers != null) - { - foreach (var n in _servers) - { - _addresses.Add(new ConfigServerAddress(n)); - } - } - } - - return _addresses; - } + get { return _addresses; } } } } diff --git a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs new file mode 100644 index 0000000000..ed472ea35a --- /dev/null +++ b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs @@ -0,0 +1,385 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Web; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Umbraco.Core.Cache; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; +using Umbraco.Core.Models.Rdbms; +using Umbraco.Core.Persistence; +using umbraco.interfaces; + +namespace Umbraco.Core.Sync +{ + /// + /// An that works by storing messages in the database. + /// + // + // abstract because it needs to be inherited by a class that will + // - trigger Boot() when appropriate + // - trigger Sync() when appropriate + // + // this messenger writes ALL instructions to the database, + // but only processes instructions coming from remote servers, + // thus ensuring that instructions run only once + // + public abstract class DatabaseServerMessenger : ServerMessengerBase + { + private readonly ApplicationContext _appContext; + private readonly DatabaseServerMessengerOptions _options; + private readonly object _lock = new object(); + private int _lastId = -1; + private volatile bool _syncing; + private DateTime _lastSync; + private bool _initialized; + + protected ApplicationContext ApplicationContext { get { return _appContext; } } + + protected DatabaseServerMessenger(ApplicationContext appContext, bool distributedEnabled, DatabaseServerMessengerOptions options) + : base(distributedEnabled) + { + if (appContext == null) throw new ArgumentNullException("appContext"); + if (options == null) throw new ArgumentNullException("options"); + + _appContext = appContext; + _options = options; + _lastSync = DateTime.UtcNow; + } + + #region Messenger + + protected override bool RequiresDistributed(IEnumerable servers, ICacheRefresher refresher, MessageType dispatchType) + { + // we don't care if there's servers listed or not, + // if distributed call is enabled we will make the call + return _initialized && DistributedEnabled; + } + + protected override void DeliverRemote( + IEnumerable servers, + ICacheRefresher refresher, + MessageType messageType, + IEnumerable ids = null, + string json = null) + { + var idsA = ids == null ? null : ids.ToArray(); + + Type idType; + if (GetArrayType(idsA, out idType) == false) + throw new ArgumentException("All items must be of the same type, either int or Guid.", "ids"); + + var instructions = RefreshInstruction.GetInstructions(refresher, messageType, idsA, idType, json); + + var dto = new CacheInstructionDto + { + UtcStamp = DateTime.UtcNow, + Instructions = JsonConvert.SerializeObject(instructions, Formatting.None), + OriginIdentity = GetLocalIdentity() + }; + + ApplicationContext.DatabaseContext.Database.Insert(dto); + } + + #endregion + + #region Sync + + /// + /// Boots the messenger. + /// + /// + /// Thread safety: this is NOT thread safe. Because it is NOT meant to run multi-threaded. + /// Callers MUST ensure thread-safety. + /// + protected void Boot() + { + ReadLastSynced(); + if (_lastId < 0) // never synced before + Initialize(); + } + + /// + /// Initializes a server that has never synchronized before. + /// + /// + /// Thread safety: this is NOT thread safe. Because it is NOT meant to run multi-threaded. + /// + private void Initialize() + { + // we haven't synced - in this case we aren't going to sync the whole thing, we will assume this is a new + // server and it will need to rebuild it's own caches, eg Lucene or the xml cache file. + LogHelper.Warn("No last synced Id found, this generally means this is a new server/install. The server will rebuild its caches and indexes and then adjust it's last synced id to the latest found in the database and will start maintaining cache updates based on that id"); + + // go get the last id in the db and store it + // note: do it BEFORE initializing otherwise some instructions might get lost + // when doing it before, some instructions might run twice - not an issue + var lastId = _appContext.DatabaseContext.Database.ExecuteScalar("SELECT MAX(id) FROM umbracoCacheInstruction"); + if (lastId > 0) + SaveLastSynced(lastId); + + // execute initializing callbacks + if (_options.InitializingCallbacks != null) + foreach (var callback in _options.InitializingCallbacks) + callback(); + + _initialized = true; + } + + /// + /// Synchronize the server (throttled). + /// + protected void Sync() + { + if ((DateTime.UtcNow - _lastSync).Seconds <= _options.ThrottleSeconds) + return; + + if (_syncing) return; + + lock (_lock) + { + if (_syncing) return; + + _syncing = true; // lock other threads out + _lastSync = DateTime.UtcNow; + + using (DisposableTimer.DebugDuration("Syncing from database...")) + { + ProcessDatabaseInstructions(); + PruneOldInstructions(); + } + + _syncing = false; // release + } + } + + /// + /// Process instructions from the database. + /// + /// + /// Thread safety: this is NOT thread safe. Because it is NOT meant to run multi-threaded. + /// + private void ProcessDatabaseInstructions() + { + // NOTE + // we 'could' recurse to ensure that no remaining instructions are pending in the table before proceeding but I don't think that + // would be a good idea since instructions could keep getting added and then all other threads will probably get stuck from serving requests + // (depending on what the cache refreshers are doing). I think it's best we do the one time check, process them and continue, if there are + // pending requests after being processed, they'll just be processed on the next poll. + // + // FIXME not true if we're running on a background thread, assuming we can? + + var sql = new Sql().Select("*") + .From() + .Where(dto => dto.Id > _lastId) + .OrderBy(dto => dto.Id); + + var dtos = _appContext.DatabaseContext.Database.Fetch(sql); + if (dtos.Count <= 0) return; + + // only process instructions coming from a remote server, and ignore instructions coming from + // the local server as they've already been processed. We should NOT assume that the sequence of + // instructions in the database makes any sense whatsoever, because it's all async. + var localIdentity = GetLocalIdentity(); + var remoteDtos = dtos.Where(x => x.OriginIdentity != localIdentity); + + var lastId = 0; + foreach (var dto in remoteDtos) + { + try + { + var jsonArray = JsonConvert.DeserializeObject(dto.Instructions); + NotifyRefreshers(jsonArray); + lastId = dto.Id; + } + catch (JsonException ex) + { + // FIXME + // if we cannot deserialize then it's OK to skip the instructions + // but what if NotifyRefreshers throws?! + + LogHelper.Error("Could not deserialize a distributed cache instruction (\"" + dto.Instructions + "\").", ex); + } + } + + if (lastId > 0) + SaveLastSynced(lastId); + } + + /// + /// Remove old instructions from the database. + /// + private void PruneOldInstructions() + { + _appContext.DatabaseContext.Database.Delete("WHERE utcStamp < @pruneDate", + new { pruneDate = DateTime.UtcNow.AddDays(-_options.DaysToRetainInstructions) }); + } + + /// + /// Reads the last-synced id from file into memory. + /// + /// + /// Thread safety: this is NOT thread safe. Because it is NOT meant to run multi-threaded. + /// + private void ReadLastSynced() + { + var path = SyncFilePath; + if (File.Exists(path) == false) return; + + var content = File.ReadAllText(path); + int last; + if (int.TryParse(content, out last)) + _lastId = last; + } + + /// + /// Updates the in-memory last-synced id and persists it to file. + /// + /// The id. + /// + /// Thread safety: this is NOT thread safe. Because it is NOT meant to run multi-threaded. + /// + private void SaveLastSynced(int id) + { + File.WriteAllText(SyncFilePath, id.ToString(CultureInfo.InvariantCulture)); + _lastId = id; + } + + /// + /// Gets the local server unique identity. + /// + /// The unique identity of the local server. + protected string GetLocalIdentity() + { + return JsonConvert.SerializeObject(new + { + machineName = NetworkHelper.MachineName, + appDomainAppId = HttpRuntime.AppDomainAppId + }); + } + + /// + /// Gets the sync file path for the local server. + /// + /// The sync file path for the local server. + private static string SyncFilePath + { + get + { + var tempFolder = IOHelper.MapPath("~/App_Data/TEMP/DistCache/" + NetworkHelper.FileSafeMachineName); + if (Directory.Exists(tempFolder) == false) + Directory.CreateDirectory(tempFolder); + + return Path.Combine(tempFolder, HttpRuntime.AppDomainAppId.ReplaceNonAlphanumericChars(string.Empty) + "-lastsynced.txt"); + } + } + + #endregion + + #region Notify refreshers + + private static ICacheRefresher GetRefresher(Guid id) + { + var refresher = CacheRefreshersResolver.Current.GetById(id); + if (refresher == null) + throw new InvalidOperationException("Cache refresher with ID \"" + id + "\" does not exist."); + return refresher; + } + + private static IJsonCacheRefresher GetJsonRefresher(Guid id) + { + return GetJsonRefresher(GetRefresher(id)); + } + + private static IJsonCacheRefresher GetJsonRefresher(ICacheRefresher refresher) + { + var jsonRefresher = refresher as IJsonCacheRefresher; + if (jsonRefresher == null) + throw new InvalidOperationException("Cache refresher with ID \"" + refresher.UniqueIdentifier + "\" does not implement " + typeof(IJsonCacheRefresher) + "."); + return jsonRefresher; + } + + private static void NotifyRefreshers(IEnumerable jsonArray) + { + foreach (var jsonItem in jsonArray) + { + // could be a JObject in which case we can convert to a RefreshInstruction, + // otherwise it could be another JArray - in which case we'll iterate that. + var jsonObj = jsonItem as JObject; + if (jsonObj != null) + { + var instruction = jsonObj.ToObject(); + switch (instruction.RefreshType) + { + case RefreshMethodType.RefreshAll: + RefreshAll(instruction.RefresherId); + break; + case RefreshMethodType.RefreshByGuid: + RefreshByGuid(instruction.RefresherId, instruction.GuidId); + break; + case RefreshMethodType.RefreshById: + RefreshById(instruction.RefresherId, instruction.IntId); + break; + case RefreshMethodType.RefreshByIds: + RefreshByIds(instruction.RefresherId, instruction.JsonIds); + break; + case RefreshMethodType.RefreshByJson: + RefreshByJson(instruction.RefresherId, instruction.JsonPayload); + break; + case RefreshMethodType.RemoveById: + RemoveById(instruction.RefresherId, instruction.IntId); + break; + } + + } + else + { + var jsonInnerArray = (JArray) jsonItem; + NotifyRefreshers(jsonInnerArray); // recurse + } + } + } + + private static void RefreshAll(Guid uniqueIdentifier) + { + var refresher = GetRefresher(uniqueIdentifier); + refresher.RefreshAll(); + } + + private static void RefreshByGuid(Guid uniqueIdentifier, Guid id) + { + var refresher = GetRefresher(uniqueIdentifier); + refresher.Refresh(id); + } + + private static void RefreshById(Guid uniqueIdentifier, int id) + { + var refresher = GetRefresher(uniqueIdentifier); + refresher.Refresh(id); + } + + private static void RefreshByIds(Guid uniqueIdentifier, string jsonIds) + { + var refresher = GetRefresher(uniqueIdentifier); + foreach (var id in JsonConvert.DeserializeObject(jsonIds)) + refresher.Refresh(id); + } + + private static void RefreshByJson(Guid uniqueIdentifier, string jsonPayload) + { + var refresher = GetJsonRefresher(uniqueIdentifier); + refresher.Refresh(jsonPayload); + } + + private static void RemoveById(Guid uniqueIdentifier, int id) + { + var refresher = GetRefresher(uniqueIdentifier); + refresher.Remove(id); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Sync/DatabaseServerMessengerOptions.cs b/src/Umbraco.Core/Sync/DatabaseServerMessengerOptions.cs new file mode 100644 index 0000000000..66b845f4ec --- /dev/null +++ b/src/Umbraco.Core/Sync/DatabaseServerMessengerOptions.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; + +namespace Umbraco.Core.Sync +{ + /// + /// Provides options to the . + /// + public class DatabaseServerMessengerOptions + { + /// + /// Initializes a new instance of the with default values. + /// + public DatabaseServerMessengerOptions() + { + DaysToRetainInstructions = 100; // 100 days + ThrottleSeconds = 5; // 5 seconds + } + + /// + /// A list of callbacks that will be invoked if the lastsynced.txt file does not exist. + /// + /// + /// These callbacks will typically be for eg rebuilding the xml cache file, or examine indexes, based on + /// the data in the database to get this particular server node up to date. + /// + public IEnumerable InitializingCallbacks { get; set; } + + /// + /// The number of days to keep instructions in the database; records older than this number will be pruned. + /// + public int DaysToRetainInstructions { get; set; } + + /// + /// The number of seconds to wait between each sync operations. + /// + public int ThrottleSeconds { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Sync/DatabaseServerRegistrar.cs b/src/Umbraco.Core/Sync/DatabaseServerRegistrar.cs index bce812edd8..e2f400ea71 100644 --- a/src/Umbraco.Core/Sync/DatabaseServerRegistrar.cs +++ b/src/Umbraco.Core/Sync/DatabaseServerRegistrar.cs @@ -4,19 +4,35 @@ using Umbraco.Core.Services; namespace Umbraco.Core.Sync { - /// - /// A registrar that stores registered server nodes in a database + /// A registrar that stores registered server nodes in the database. /// - internal class DatabaseServerRegistrar : IServerRegistrar + internal sealed class DatabaseServerRegistrar : IServerRegistrar { private readonly Lazy _registrationService; - public DatabaseServerRegistrar(Lazy registrationService) + /// + /// Gets or sets the registrar options. + /// + public DatabaseServerRegistrarOptions Options { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + /// The registration service. + /// Some options. + public DatabaseServerRegistrar(Lazy registrationService, DatabaseServerRegistrarOptions options) { + if (registrationService == null) throw new ArgumentNullException("registrationService"); + if (options == null) throw new ArgumentNullException("options"); + + Options = options; _registrationService = registrationService; } + /// + /// Gets the registered servers. + /// public IEnumerable Registrations { get { return _registrationService.Value.GetActiveServers(); } diff --git a/src/Umbraco.Core/Sync/DatabaseServerRegistrarOptions.cs b/src/Umbraco.Core/Sync/DatabaseServerRegistrarOptions.cs new file mode 100644 index 0000000000..4ee7fec371 --- /dev/null +++ b/src/Umbraco.Core/Sync/DatabaseServerRegistrarOptions.cs @@ -0,0 +1,29 @@ +using System; + +namespace Umbraco.Core.Sync +{ + /// + /// Provides options to the . + /// + public sealed class DatabaseServerRegistrarOptions + { + /// + /// Initializes a new instance of the class with default values. + /// + public DatabaseServerRegistrarOptions() + { + StaleServerTimeout = new TimeSpan(1,0,0); // 1 day + ThrottleSeconds = 30; // 30 seconds + } + + /// + /// The number of seconds to wait between each updates to the database. + /// + public int ThrottleSeconds { get; set; } + + /// + /// The time span to wait before considering a server stale, after it has last been accessed. + /// + public TimeSpan StaleServerTimeout { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Sync/DefaultServerMessenger.cs b/src/Umbraco.Core/Sync/DefaultServerMessenger.cs deleted file mode 100644 index f302dbe4d8..0000000000 --- a/src/Umbraco.Core/Sync/DefaultServerMessenger.cs +++ /dev/null @@ -1,552 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Net; -using System.Threading; -using System.Web; -using System.Web.Script.Serialization; -using Umbraco.Core.Cache; -using Umbraco.Core.Configuration; -using Umbraco.Core.Logging; -using umbraco.interfaces; - -namespace Umbraco.Core.Sync -{ - /// - /// The default server messenger that uses web services to keep servers in sync - /// - internal class DefaultServerMessenger : IServerMessenger - { - private readonly Func> _getUserNamePasswordDelegate; - private volatile bool _hasResolvedDelegate = false; - private readonly object _locker = new object(); - - protected string Login { get; private set; } - protected string Password{ get; private set; } - - protected bool UseDistributedCalls { get; private set; } - - /// - /// Without a username/password all distribuion will be disabled - /// - internal DefaultServerMessenger() - { - UseDistributedCalls = false; - } - - /// - /// Distribution will be enabled based on the umbraco config setting. - /// - /// - /// - internal DefaultServerMessenger(string login, string password) - : this(login, password, UmbracoConfig.For.UmbracoSettings().DistributedCall.Enabled) - { - } - - /// - /// Specifies the username/password and whether or not to use distributed calls - /// - /// - /// - /// - internal DefaultServerMessenger(string login, string password, bool useDistributedCalls) - { - if (login == null) throw new ArgumentNullException("login"); - if (password == null) throw new ArgumentNullException("password"); - - UseDistributedCalls = useDistributedCalls; - Login = login; - Password = password; - } - - /// - /// Allows to set a lazy delegate to resolve the username/password - /// - /// - public DefaultServerMessenger(Func> getUserNamePasswordDelegate) - { - _getUserNamePasswordDelegate = getUserNamePasswordDelegate; - } - - public void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, string jsonPayload) - { - if (servers == null) throw new ArgumentNullException("servers"); - if (refresher == null) throw new ArgumentNullException("refresher"); - if (jsonPayload == null) throw new ArgumentNullException("jsonPayload"); - - MessageSeversForIdsOrJson(servers, refresher, MessageType.RefreshByJson, jsonPayload: jsonPayload); - } - - public void PerformRefresh(IEnumerable servers, ICacheRefresher refresher,Func getNumericId, params T[] instances) - { - if (servers == null) throw new ArgumentNullException("servers"); - if (refresher == null) throw new ArgumentNullException("refresher"); - - //copy local - var idGetter = getNumericId; - - MessageSeversForManyObjects(servers, refresher, MessageType.RefreshById, - x => idGetter(x), - instances); - } - - public void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, Func getGuidId, params T[] instances) - { - if (servers == null) throw new ArgumentNullException("servers"); - if (refresher == null) throw new ArgumentNullException("refresher"); - - //copy local - var idGetter = getGuidId; - - MessageSeversForManyObjects(servers, refresher, MessageType.RefreshById, - x => idGetter(x), - instances); - } - - public void PerformRemove(IEnumerable servers, ICacheRefresher refresher, Func getNumericId, params T[] instances) - { - if (servers == null) throw new ArgumentNullException("servers"); - if (refresher == null) throw new ArgumentNullException("refresher"); - - //copy local - var idGetter = getNumericId; - - MessageSeversForManyObjects(servers, refresher, MessageType.RemoveById, - x => idGetter(x), - instances); - } - - public void PerformRemove(IEnumerable servers, ICacheRefresher refresher, params int[] numericIds) - { - if (servers == null) throw new ArgumentNullException("servers"); - if (refresher == null) throw new ArgumentNullException("refresher"); - - MessageSeversForIdsOrJson(servers, refresher, MessageType.RemoveById, numericIds.Cast()); - } - - public void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, params int[] numericIds) - { - if (servers == null) throw new ArgumentNullException("servers"); - if (refresher == null) throw new ArgumentNullException("refresher"); - - MessageSeversForIdsOrJson(servers, refresher, MessageType.RefreshById, numericIds.Cast()); - } - - public void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, params Guid[] guidIds) - { - if (servers == null) throw new ArgumentNullException("servers"); - if (refresher == null) throw new ArgumentNullException("refresher"); - - MessageSeversForIdsOrJson(servers, refresher, MessageType.RefreshById, guidIds.Cast()); - } - - public void PerformRefreshAll(IEnumerable servers, ICacheRefresher refresher) - { - MessageSeversForIdsOrJson(servers, refresher, MessageType.RefreshAll, Enumerable.Empty().ToArray()); - } - - private void InvokeMethodOnRefresherInstance(ICacheRefresher refresher, MessageType dispatchType, Func getId, IEnumerable instances) - { - if (refresher == null) throw new ArgumentNullException("refresher"); - - LogHelper.Debug("Invoking refresher {0} on single server instance, message type {1}", - () => refresher.GetType(), - () => dispatchType); - - var stronglyTypedRefresher = refresher as ICacheRefresher; - - foreach (var instance in instances) - { - //if we are not, then just invoke the call on the cache refresher - switch (dispatchType) - { - case MessageType.RefreshAll: - refresher.RefreshAll(); - break; - case MessageType.RefreshById: - if (stronglyTypedRefresher != null) - { - stronglyTypedRefresher.Refresh(instance); - } - else - { - var id = getId(instance); - if (id is int) - { - refresher.Refresh((int)id); - } - else if (id is Guid) - { - refresher.Refresh((Guid)id); - } - else - { - throw new InvalidOperationException("The id must be either an int or a Guid"); - } - } - break; - case MessageType.RemoveById: - if (stronglyTypedRefresher != null) - { - stronglyTypedRefresher.Remove(instance); - } - else - { - var id = getId(instance); - refresher.Refresh((int)id); - } - break; - } - } - } - - /// - /// If we are instantiated with a lazy delegate to get the username/password, we'll resolve it here - /// - private void EnsureLazyUsernamePasswordDelegateResolved() - { - if (!_hasResolvedDelegate && _getUserNamePasswordDelegate != null) - { - lock (_locker) - { - if (!_hasResolvedDelegate) - { - _hasResolvedDelegate = true; //set flag - - try - { - var result = _getUserNamePasswordDelegate(); - if (result == null) - { - Login = null; - Password = null; - UseDistributedCalls = false; - } - else - { - Login = result.Item1; - Password = result.Item2; - UseDistributedCalls = UmbracoConfig.For.UmbracoSettings().DistributedCall.Enabled; - } - } - catch (Exception ex) - { - LogHelper.Error("Could not resolve username/password delegate, server distribution will be disabled", ex); - Login = null; - Password = null; - UseDistributedCalls = false; - } - } - } - } - } - - protected void InvokeMethodOnRefresherInstance(ICacheRefresher refresher, MessageType dispatchType, IEnumerable ids = null, string jsonPayload = null) - { - if (refresher == null) throw new ArgumentNullException("refresher"); - - LogHelper.Debug("Invoking refresher {0} on single server instance, message type {1}", - () => refresher.GetType(), - () => dispatchType); - - //if it is a refresh all we'll do it here since ids will be null or empty - if (dispatchType == MessageType.RefreshAll) - { - refresher.RefreshAll(); - } - else - { - if (ids != null) - { - foreach (var id in ids) - { - //if we are not, then just invoke the call on the cache refresher - switch (dispatchType) - { - case MessageType.RefreshById: - if (id is int) - { - refresher.Refresh((int) id); - } - else if (id is Guid) - { - refresher.Refresh((Guid) id); - } - else - { - throw new InvalidOperationException("The id must be either an int or a Guid"); - } - - break; - case MessageType.RemoveById: - refresher.Remove((int) id); - break; - } - } - } - else - { - //we can only proceed if the cache refresher is IJsonCacheRefresher! - var jsonRefresher = refresher as IJsonCacheRefresher; - if (jsonRefresher == null) - { - throw new InvalidOperationException("The cache refresher " + refresher.GetType() + " is not of type " + typeof(IJsonCacheRefresher)); - } - - //if we are not, then just invoke the call on the cache refresher - jsonRefresher.Refresh(jsonPayload); - } - } - } - - private void MessageSeversForManyObjects( - IEnumerable servers, - ICacheRefresher refresher, - MessageType dispatchType, - Func getId, - IEnumerable instances) - { - if (servers == null) throw new ArgumentNullException("servers"); - if (refresher == null) throw new ArgumentNullException("refresher"); - - EnsureLazyUsernamePasswordDelegateResolved(); - - //Now, check if we are using Distrubuted calls. If there are no servers in the list then we - // can definitely not distribute. - if (!UseDistributedCalls || !servers.Any()) - { - //if we are not, then just invoke the call on the cache refresher - InvokeMethodOnRefresherInstance(refresher, dispatchType, getId, instances); - return; - } - - //if we are distributing calls then we'll need to do it by id - MessageSeversForIdsOrJson(servers, refresher, dispatchType, instances.Select(getId)); - } - - protected virtual void MessageSeversForIdsOrJson( - IEnumerable servers, - ICacheRefresher refresher, - MessageType dispatchType, - IEnumerable ids = null, - string jsonPayload = null) - { - if (servers == null) throw new ArgumentNullException("servers"); - if (refresher == null) throw new ArgumentNullException("refresher"); - - Type arrayType; - if (!ValidateIdArray(ids, out arrayType)) - { - throw new ArgumentException("The id must be either an int or a Guid"); - } - - EnsureLazyUsernamePasswordDelegateResolved(); - - //Now, check if we are using Distrubuted calls. If there are no servers in the list then we - // can definitely not distribute. - if (!UseDistributedCalls || !servers.Any()) - { - //if we are not, then just invoke the call on the cache refresher - InvokeMethodOnRefresherInstance(refresher, dispatchType, ids, jsonPayload); - return; - } - - LogHelper.Debug( - "Performing distributed call for refresher {0}, message type: {1}, servers: {2}, ids: {3}, json: {4}", - refresher.GetType, - () => dispatchType, - () => string.Join(";", servers.Select(x => x.ToString())), - () => ids == null ? "" : string.Join(";", ids.Select(x => x.ToString())), - () => jsonPayload ?? ""); - - PerformDistributedCall(servers, refresher, dispatchType, ids, arrayType, jsonPayload); - } - - private bool ValidateIdArray(IEnumerable ids, out Type arrayType) - { - arrayType = null; - if (ids != null) - { - foreach (var id in ids) - { - if (!(id is int) && (!(id is Guid))) - return false; // - if (arrayType == null) - arrayType = id.GetType(); - if (arrayType != id.GetType()) - throw new ArgumentException("The array must contain the same type of " + arrayType); - } - } - return true; - } - - protected virtual void PerformDistributedCall( - IEnumerable servers, - ICacheRefresher refresher, - MessageType dispatchType, - IEnumerable ids = null, - Type idArrayType = null, - string jsonPayload = null) - { - //We are using distributed calls, so lets make them... - try - { - - //TODO: We should try to figure out the current server's address and if it matches any of the ones - // in the ServerAddress list, then just refresh directly on this server and exclude that server address - // from the list, this will save an internal request. - - using (var cacheRefresher = new ServerSyncWebServiceClient()) - { - var asyncResultsList = new List(); - - LogStartDispatch(); - - // Go through each configured node submitting a request asynchronously - //NOTE: 'asynchronously' in this case does not mean that it will continue while we give the page back to the user! - foreach (var n in servers) - { - //set the server address - cacheRefresher.Url = n.ServerAddress; - - // Add the returned WaitHandle to the list for later checking - switch (dispatchType) - { - case MessageType.RefreshByJson: - asyncResultsList.Add( - cacheRefresher.BeginRefreshByJson( - refresher.UniqueIdentifier, jsonPayload, Login, Password, null, null)); - break; - case MessageType.RefreshAll: - asyncResultsList.Add( - cacheRefresher.BeginRefreshAll( - refresher.UniqueIdentifier, Login, Password, null, null)); - break; - case MessageType.RefreshById: - if (idArrayType == null) - { - throw new InvalidOperationException("Cannot refresh by id if the idArrayType is null"); - } - - if (idArrayType == typeof(int)) - { - var serializer = new JavaScriptSerializer(); - var jsonIds = serializer.Serialize(ids.Cast().ToArray()); - //we support bulk loading of Integers - var result = cacheRefresher.BeginRefreshByIds(refresher.UniqueIdentifier, jsonIds, Login, Password, null, null); - asyncResultsList.Add(result); - } - else - { - //we don't currently support bulk loading of GUIDs (not even sure if we have any Guid ICacheRefreshers) - //so we'll just iterate - asyncResultsList.AddRange( - ids.Select(i => cacheRefresher.BeginRefreshByGuid( - refresher.UniqueIdentifier, (Guid)i, Login, Password, null, null))); - } - - break; - case MessageType.RemoveById: - //we don't currently support bulk removing so we'll iterate - asyncResultsList.AddRange( - ids.Select(i => cacheRefresher.BeginRemoveById( - refresher.UniqueIdentifier, (int)i, Login, Password, null, null))); - break; - } - } - - var waitHandlesList = asyncResultsList.Select(x => x.AsyncWaitHandle).ToArray(); - - var errorCount = 0; - - //Wait for all requests to complete - WaitHandle.WaitAll(waitHandlesList.ToArray()); - - foreach (var t in asyncResultsList) - { - //var handleIndex = WaitHandle.WaitAny(waitHandlesList.ToArray(), TimeSpan.FromSeconds(15)); - - try - { - // Find out if the call succeeded - switch (dispatchType) - { - case MessageType.RefreshByJson: - cacheRefresher.EndRefreshByJson(t); - break; - case MessageType.RefreshAll: - cacheRefresher.EndRefreshAll(t); - break; - case MessageType.RefreshById: - if (idArrayType == null) - { - throw new InvalidOperationException("Cannot refresh by id if the idArrayType is null"); - } - - if (idArrayType == typeof(int)) - { - cacheRefresher.EndRefreshById(t); - } - else - { - cacheRefresher.EndRefreshByGuid(t); - } - break; - case MessageType.RemoveById: - cacheRefresher.EndRemoveById(t); - break; - } - } - catch (WebException ex) - { - LogDispatchNodeError(ex); - - errorCount++; - } - catch (Exception ex) - { - LogDispatchNodeError(ex); - - errorCount++; - } - } - - LogDispatchBatchResult(errorCount); - } - } - catch (Exception ee) - { - LogDispatchBatchError(ee); - } - } - - private void LogDispatchBatchError(Exception ee) - { - LogHelper.Error("Error refreshing distributed list", ee); - } - - private void LogDispatchBatchResult(int errorCount) - { - LogHelper.Debug(string.Format("Distributed server push completed with {0} nodes reporting an error", errorCount == 0 ? "no" : errorCount.ToString(CultureInfo.InvariantCulture))); - } - - private void LogDispatchNodeError(Exception ex) - { - LogHelper.Error("Error refreshing a node in the distributed list", ex); - } - - private void LogDispatchNodeError(WebException ex) - { - string url = (ex.Response != null) ? ex.Response.ResponseUri.ToString() : "invalid url (responseUri null)"; - LogHelper.Error("Error refreshing a node in the distributed list, URI attempted: " + url, ex); - } - - private void LogStartDispatch() - { - LogHelper.Info("Submitting calls to distributed servers"); - } - - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Sync/IServerAddress.cs b/src/Umbraco.Core/Sync/IServerAddress.cs index 0483af1800..84a3563c60 100644 --- a/src/Umbraco.Core/Sync/IServerAddress.cs +++ b/src/Umbraco.Core/Sync/IServerAddress.cs @@ -3,10 +3,13 @@ namespace Umbraco.Core.Sync { /// - /// An interface exposing a server address to use for server syncing + /// Provides the address of a server. /// public interface IServerAddress { + /// + /// Gets the server address. + /// string ServerAddress { get; } //TODO : Should probably add things like port, protocol, server name, app id diff --git a/src/Umbraco.Core/Sync/IServerMessenger.cs b/src/Umbraco.Core/Sync/IServerMessenger.cs index 568fb86026..100638c202 100644 --- a/src/Umbraco.Core/Sync/IServerMessenger.cs +++ b/src/Umbraco.Core/Sync/IServerMessenger.cs @@ -5,81 +5,97 @@ using umbraco.interfaces; namespace Umbraco.Core.Sync { /// - /// Defines a server messenger for server sync and distrubuted cache + /// Broadcasts distributed cache notifications to all servers of a load balanced environment. /// + /// Also ensures that the notification is processed on the local environment. public interface IServerMessenger { + // TODO + // everything we do "by JSON" means that data is serialized then deserialized on the local server + // we should stop using this, and instead use Notify() with an actual object that can be passed + // around locally, and serialized for remote messaging - but that would break backward compat ;-( + // + // and then ServerMessengerBase must be able to handle Notify(), and all messengers too + // and then ICacheRefresher (or INotifiableCacheRefresher?) must be able to handle it too + // + // >> v8 /// - /// Performs a refresh and sends along the JSON payload to each server + /// Notifies the distributed cache, for a specified . /// - /// - /// - /// - /// A pre-formatted custom json payload to be sent to the servers, the cache refresher will deserialize and use to refresh cache - /// + /// The servers that compose the load balanced environment. + /// The ICacheRefresher. + /// The notification content. void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, string jsonPayload); + ///// + ///// Notifies the distributed cache, for a specified . + ///// + ///// The servers that compose the load balanced environment. + ///// The ICacheRefresher. + ///// The notification content. + ///// A custom Json serializer. + //void Notify(IEnumerable servers, ICacheRefresher refresher, object payload, Func serializer = null); + /// - /// Performs a sync against all instance objects + /// Notifies the distributed cache of specifieds item invalidation, for a specified . /// - /// - /// The servers to sync against - /// - /// A delegate to return the Id for each instance to be used to sync to other servers - /// + /// The type of the invalidated items. + /// The servers that compose the load balanced environment. + /// The ICacheRefresher. + /// A function returning the unique identifier of items. + /// The invalidated items. void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, Func getNumericId, params T[] instances); - + /// - /// Performs a sync against all instance objects + /// Notifies the distributed cache of specifieds item invalidation, for a specified . /// - /// - /// The servers to sync against - /// - /// A delegate to return the Id for each instance to be used to sync to other servers - /// + /// The type of the invalidated items. + /// The servers that compose the load balanced environment. + /// The ICacheRefresher. + /// A function returning the unique identifier of items. + /// The invalidated items. void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, Func getGuidId, params T[] instances); /// - /// Removes the cache for the specified items + /// Notifies all servers of specified items removal, for a specified . /// - /// - /// - /// - /// A delegate to return the Id for each instance to be used to sync to other servers - /// + /// The type of the removed items. + /// The servers that compose the load balanced environment. + /// The ICacheRefresher. + /// A function returning the unique identifier of items. + /// The removed items. void PerformRemove(IEnumerable servers, ICacheRefresher refresher, Func getNumericId, params T[] instances); /// - /// Removes the cache for the specified items + /// Notifies all servers of specified items removal, for a specified . /// - /// - /// - /// + /// The servers that compose the load balanced environment. + /// The ICacheRefresher. + /// The unique identifiers of the removed items. void PerformRemove(IEnumerable servers, ICacheRefresher refresher, params int[] numericIds); /// - /// Performs a sync against all Ids + /// Notifies all servers of specified items invalidation, for a specified . /// - /// The servers to sync against - /// - /// + /// The servers that compose the load balanced environment. + /// The ICacheRefresher. + /// The unique identifiers of the invalidated items. void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, params int[] numericIds); - + /// - /// Performs a sync against all Ids + /// Notifies all servers of specified items invalidation, for a specified . /// - /// The servers to sync against - /// - /// + /// The servers that compose the load balanced environment. + /// The ICacheRefresher. + /// The unique identifiers of the invalidated items. void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, params Guid[] guidIds); /// - /// Performs entire cache refresh for a specified refresher + /// Notifies all servers of a global invalidation for a specified . /// - /// - /// + /// The servers that compose the load balanced environment. + /// The ICacheRefresher. void PerformRefreshAll(IEnumerable servers, ICacheRefresher refresher); } - } \ No newline at end of file diff --git a/src/Umbraco.Core/Sync/IServerRegistrar.cs b/src/Umbraco.Core/Sync/IServerRegistrar.cs index 46e0c268f1..5f63440859 100644 --- a/src/Umbraco.Core/Sync/IServerRegistrar.cs +++ b/src/Umbraco.Core/Sync/IServerRegistrar.cs @@ -3,10 +3,13 @@ namespace Umbraco.Core.Sync { /// - /// An interface to expose a list of server registrations for server syncing + /// Provides server registrations to the distributed cache. /// public interface IServerRegistrar { + /// + /// Gets the server registrations. + /// IEnumerable Registrations { get; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Sync/MessageType.cs b/src/Umbraco.Core/Sync/MessageType.cs index aa8733be33..80e9bcae76 100644 --- a/src/Umbraco.Core/Sync/MessageType.cs +++ b/src/Umbraco.Core/Sync/MessageType.cs @@ -1,7 +1,7 @@ namespace Umbraco.Core.Sync { /// - /// The message type to be used for syncing across servers + /// The message type to be used for syncing across servers. /// public enum MessageType { diff --git a/src/Umbraco.Core/Sync/RefreshInstruction.cs b/src/Umbraco.Core/Sync/RefreshInstruction.cs index 867266085b..a950b9bf78 100644 --- a/src/Umbraco.Core/Sync/RefreshInstruction.cs +++ b/src/Umbraco.Core/Sync/RefreshInstruction.cs @@ -1,46 +1,137 @@ using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using umbraco.interfaces; namespace Umbraco.Core.Sync { [Serializable] public class RefreshInstruction { - public RefreshMethodType RefreshType { get; set; } - public Guid RefresherId { get; set; } - public Guid GuidId { get; set; } - public int IntId { get; set; } - public string JsonIds { get; set; } - public string JsonPayload { get; set; } + // NOTE + // that class should be refactored + // but at the moment it is exposed in CacheRefresher webservice + // so for the time being we keep it as-is for backward compatibility reasons - [Serializable] - public enum RefreshMethodType + private RefreshInstruction(ICacheRefresher refresher, RefreshMethodType refreshType) { - RefreshAll, - RefreshByGuid, - RefreshById, - RefreshByIds, - RefreshByJson, - RemoveById + RefresherId = refresher.UniqueIdentifier; + RefreshType = refreshType; } + private RefreshInstruction(ICacheRefresher refresher, RefreshMethodType refreshType, Guid guidId) + : this(refresher, refreshType) + { + GuidId = guidId; + } + + private RefreshInstruction(ICacheRefresher refresher, RefreshMethodType refreshType, int intId) + : this(refresher, refreshType) + { + IntId = intId; + } + + private RefreshInstruction(ICacheRefresher refresher, RefreshMethodType refreshType, string json) + : this(refresher, refreshType) + { + if (refreshType == RefreshMethodType.RefreshByJson) + JsonPayload = json; + else + JsonIds = json; + } + + public static IEnumerable GetInstructions( + ICacheRefresher refresher, + MessageType messageType, + IEnumerable ids, + Type idType, + string json) + { + switch (messageType) + { + case MessageType.RefreshAll: + return new[] { new RefreshInstruction(refresher, RefreshMethodType.RefreshAll) }; + + case MessageType.RefreshByJson: + return new[] { new RefreshInstruction(refresher, RefreshMethodType.RefreshByJson, json) }; + + case MessageType.RefreshById: + if (idType == null) + throw new InvalidOperationException("Cannot refresh by id if idType is null."); + if (idType == typeof (int)) // bulk of ints is supported + return new[] { new RefreshInstruction(refresher, RefreshMethodType.RefreshByIds, JsonConvert.SerializeObject(ids.Cast().ToArray())) }; + // else must be guids, bulk of guids is not supported, iterate + return ids.Select(x => new RefreshInstruction(refresher, RefreshMethodType.RefreshByGuid, (Guid) x)); + + case MessageType.RemoveById: + if (idType == null) + throw new InvalidOperationException("Cannot remove by id if idType is null."); + // must be ints, bulk-remove is not supported, iterate + return ids.Select(x => new RefreshInstruction(refresher, RefreshMethodType.RemoveById, (int) x)); + //return new[] { new RefreshInstruction(refresher, RefreshMethodType.RemoveByIds, JsonConvert.SerializeObject(ids.Cast().ToArray())) }; + + default: + //case MessageType.RefreshByInstance: + //case MessageType.RemoveByInstance: + throw new ArgumentOutOfRangeException("messageType"); + } + } + + /// + /// Gets or sets the refresh action type. + /// + public RefreshMethodType RefreshType { get; set; } + + /// + /// Gets or sets the refresher unique identifier. + /// + public Guid RefresherId { get; set; } + + /// + /// Gets or sets the Guid data value. + /// + public Guid GuidId { get; set; } + + /// + /// Gets or sets the int data value. + /// + public int IntId { get; set; } + + /// + /// Gets or sets the ids data value. + /// + public string JsonIds { get; set; } + + /// + /// Gets or sets the payload data value. + /// + public string JsonPayload { get; set; } + protected bool Equals(RefreshInstruction other) { - return RefreshType == other.RefreshType && RefresherId.Equals(other.RefresherId) && GuidId.Equals(other.GuidId) && IntId == other.IntId && string.Equals(JsonIds, other.JsonIds) && string.Equals(JsonPayload, other.JsonPayload); + return RefreshType == other.RefreshType + && RefresherId.Equals(other.RefresherId) + && GuidId.Equals(other.GuidId) + && IntId == other.IntId + && string.Equals(JsonIds, other.JsonIds) + && string.Equals(JsonPayload, other.JsonPayload); } - public override bool Equals(object obj) + public override bool Equals(object other) { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((RefreshInstruction) obj); + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + if (other.GetType() != this.GetType()) return false; + return Equals((RefreshInstruction) other); } public override int GetHashCode() { unchecked { - int hashCode = (int) RefreshType; + var hashCode = (int) RefreshType; hashCode = (hashCode*397) ^ RefresherId.GetHashCode(); hashCode = (hashCode*397) ^ GuidId.GetHashCode(); hashCode = (hashCode*397) ^ IntId; @@ -57,7 +148,7 @@ namespace Umbraco.Core.Sync public static bool operator !=(RefreshInstruction left, RefreshInstruction right) { - return !Equals(left, right); + return Equals(left, right) == false; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Sync/RefreshInstructionEnvelope.cs b/src/Umbraco.Core/Sync/RefreshInstructionEnvelope.cs new file mode 100644 index 0000000000..12922d6dab --- /dev/null +++ b/src/Umbraco.Core/Sync/RefreshInstructionEnvelope.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using umbraco.interfaces; + +namespace Umbraco.Core.Sync +{ + public class RefreshInstructionEnvelope + { + public RefreshInstructionEnvelope(IEnumerable servers, ICacheRefresher refresher, IEnumerable instructions) + { + Servers = servers; + Refresher = refresher; + Instructions = instructions; + } + + public IEnumerable Servers { get; set; } + public ICacheRefresher Refresher { get; set; } + public IEnumerable Instructions { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Sync/RefreshMethodType.cs b/src/Umbraco.Core/Sync/RefreshMethodType.cs new file mode 100644 index 0000000000..4f6ad64716 --- /dev/null +++ b/src/Umbraco.Core/Sync/RefreshMethodType.cs @@ -0,0 +1,44 @@ +using System; + +namespace Umbraco.Core.Sync +{ + /// + /// Describes refresh action type. + /// + [Serializable] + public enum RefreshMethodType + { + // NOTE + // that enum should get merged somehow with MessageType and renamed somehow + // but at the moment it is exposed in CacheRefresher webservice through RefreshInstruction + // so for the time being we keep it as-is for backward compatibility reasons + + RefreshAll, + RefreshByGuid, + RefreshById, + RefreshByIds, + RefreshByJson, + RemoveById, + + // would adding values break backward compatibility? + //RemoveByIds + + // these are MessageType values + // note that AnythingByInstance are local messages and cannot be distributed + /* + RefreshAll, + RefreshById, + RefreshByJson, + RemoveById, + RefreshByInstance, + RemoveByInstance + */ + + // NOTE + // in the future we want + // RefreshAll + // RefreshById / ByInstance (support enumeration of int or guid) + // RemoveById / ByInstance (support enumeration of int or guid) + // Notify (for everything JSON) + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Sync/ServerMessengerBase.cs b/src/Umbraco.Core/Sync/ServerMessengerBase.cs new file mode 100644 index 0000000000..fed29b85a8 --- /dev/null +++ b/src/Umbraco.Core/Sync/ServerMessengerBase.cs @@ -0,0 +1,317 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Cache; +using Umbraco.Core.Logging; +using umbraco.interfaces; + +namespace Umbraco.Core.Sync +{ + /// + /// Provides a base class for all implementations. + /// + public abstract class ServerMessengerBase : IServerMessenger + { + protected bool DistributedEnabled { get; set; } + + protected ServerMessengerBase(bool distributedEnabled) + { + DistributedEnabled = distributedEnabled; + } + + /// + /// Determines whether to make distributed calls when messaging a cache refresher. + /// + /// The registered servers. + /// The cache refresher. + /// The message type. + /// true if distributed calls are required; otherwise, false, all we have is the local server. + protected virtual bool RequiresDistributed(IEnumerable servers, ICacheRefresher refresher, MessageType messageType) + { + return DistributedEnabled && servers.Any(); + } + + // ensures that all items in the enumerable are of the same type, either int or Guid. + protected static bool GetArrayType(IEnumerable ids, out Type arrayType) + { + arrayType = null; + if (ids == null) return true; + + foreach (var id in ids) + { + // only int and Guid are supported + if ((id is int) == false && ((id is Guid) == false)) + return false; + // initialize with first item + if (arrayType == null) + arrayType = id.GetType(); + // check remaining items + if (arrayType != id.GetType()) + return false; + } + + return true; + } + + #region IServerMessenger + + public void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, string jsonPayload) + { + if (servers == null) throw new ArgumentNullException("servers"); + if (refresher == null) throw new ArgumentNullException("refresher"); + if (jsonPayload == null) throw new ArgumentNullException("jsonPayload"); + + Deliver(servers, refresher, MessageType.RefreshByJson, json: jsonPayload); + } + + public void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, Func getNumericId, params T[] instances) + { + if (servers == null) throw new ArgumentNullException("servers"); + if (refresher == null) throw new ArgumentNullException("refresher"); + if (getNumericId == null) throw new ArgumentNullException("getNumericId"); + if (instances == null || instances.Length == 0) return; + + Func getId = x => getNumericId(x); + Deliver(servers, refresher, MessageType.RefreshByInstance, getId, instances); + } + + public void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, Func getGuidId, params T[] instances) + { + if (servers == null) throw new ArgumentNullException("servers"); + if (refresher == null) throw new ArgumentNullException("refresher"); + if (getGuidId == null) throw new ArgumentNullException("getGuidId"); + if (instances == null || instances.Length == 0) return; + + Func getId = x => getGuidId(x); + Deliver(servers, refresher, MessageType.RefreshByInstance, getId, instances); + } + + public void PerformRemove(IEnumerable servers, ICacheRefresher refresher, Func getNumericId, params T[] instances) + { + if (servers == null) throw new ArgumentNullException("servers"); + if (refresher == null) throw new ArgumentNullException("refresher"); + if (getNumericId == null) throw new ArgumentNullException("getNumericId"); + if (instances == null || instances.Length == 0) return; + + Func getId = x => getNumericId(x); + Deliver(servers, refresher, MessageType.RemoveByInstance, getId, instances); + } + + public void PerformRemove(IEnumerable servers, ICacheRefresher refresher, params int[] numericIds) + { + if (servers == null) throw new ArgumentNullException("servers"); + if (refresher == null) throw new ArgumentNullException("refresher"); + if (numericIds == null || numericIds.Length == 0) return; + + Deliver(servers, refresher, MessageType.RemoveById, numericIds.Cast()); + } + + public void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, params int[] numericIds) + { + if (servers == null) throw new ArgumentNullException("servers"); + if (refresher == null) throw new ArgumentNullException("refresher"); + if (numericIds == null || numericIds.Length == 0) return; + + Deliver(servers, refresher, MessageType.RefreshById, numericIds.Cast()); + } + + public void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, params Guid[] guidIds) + { + if (servers == null) throw new ArgumentNullException("servers"); + if (refresher == null) throw new ArgumentNullException("refresher"); + if (guidIds == null || guidIds.Length == 0) return; + + Deliver(servers, refresher, MessageType.RefreshById, guidIds.Cast()); + } + + public void PerformRefreshAll(IEnumerable servers, ICacheRefresher refresher) + { + if (servers == null) throw new ArgumentNullException("servers"); + if (refresher == null) throw new ArgumentNullException("refresher"); + + Deliver(servers, refresher, MessageType.RefreshAll); + } + + //public void PerformNotify(IEnumerable servers, ICacheRefresher refresher, object payload) + //{ + // if (servers == null) throw new ArgumentNullException("servers"); + // if (refresher == null) throw new ArgumentNullException("refresher"); + + // Deliver(servers, refresher, payload); + //} + + #endregion + + #region Deliver + + protected void DeliverLocal(ICacheRefresher refresher, MessageType messageType, IEnumerable ids = null, string json = null) + { + if (refresher == null) throw new ArgumentNullException("refresher"); + + LogHelper.Debug("Invoking refresher {0} on local server for message type {1}", + refresher.GetType, + () => messageType); + + switch (messageType) + { + case MessageType.RefreshAll: + refresher.RefreshAll(); + break; + + case MessageType.RefreshById: + if (ids != null) + foreach (var id in ids) + { + if (id is int) + refresher.Refresh((int) id); + else if (id is Guid) + refresher.Refresh((Guid) id); + else + throw new InvalidOperationException("The id must be either an int or a Guid."); + } + break; + + case MessageType.RefreshByJson: + var jsonRefresher = refresher as IJsonCacheRefresher; + if (jsonRefresher == null) + throw new InvalidOperationException("The cache refresher " + refresher.GetType() + " is not of type " + typeof(IJsonCacheRefresher)); + jsonRefresher.Refresh(json); + break; + + case MessageType.RemoveById: + if (ids != null) + foreach (var id in ids) + { + if (id is int) + refresher.Remove((int) id); + else + throw new InvalidOperationException("The id must be an int."); + } + break; + + default: + //case MessageType.RefreshByInstance: + //case MessageType.RemoveByInstance: + throw new NotSupportedException("Invalid message type " + messageType); + } + } + + protected void DeliverLocal(ICacheRefresher refresher, MessageType messageType, Func getId, IEnumerable instances) + { + if (refresher == null) throw new ArgumentNullException("refresher"); + + LogHelper.Debug("Invoking refresher {0} on local server for message type {1}", + refresher.GetType, + () => messageType); + + var typedRefresher = refresher as ICacheRefresher; + + switch (messageType) + { + case MessageType.RefreshAll: + refresher.RefreshAll(); + break; + + case MessageType.RefreshByInstance: + if (typedRefresher == null) + throw new InvalidOperationException("The refresher must be a typed refresher."); + foreach (var instance in instances) + typedRefresher.Refresh(instance); + break; + + case MessageType.RemoveByInstance: + if (typedRefresher == null) + throw new InvalidOperationException("The cache refresher " + refresher.GetType() + " is not a typed refresher."); + foreach (var instance in instances) + typedRefresher.Remove(instance); + break; + + default: + //case MessageType.RefreshById: + //case MessageType.RemoveById: + //case MessageType.RefreshByJson: + throw new NotSupportedException("Invalid message type " + messageType); + } + } + + //protected void DeliverLocal(ICacheRefresher refresher, object payload) + //{ + // if (refresher == null) throw new ArgumentNullException("refresher"); + + // LogHelper.Debug("Invoking refresher {0} on local server for message type Notify", + // () => refresher.GetType()); + + // refresher.Notify(payload); + //} + + protected abstract void DeliverRemote(IEnumerable servers, ICacheRefresher refresher, MessageType messageType, IEnumerable ids = null, string json = null); + + //protected abstract void DeliverRemote(IEnumerable servers, ICacheRefresher refresher, object payload); + + protected virtual void Deliver(IEnumerable servers, ICacheRefresher refresher, MessageType messageType, IEnumerable ids = null, string json = null) + { + if (servers == null) throw new ArgumentNullException("servers"); + if (refresher == null) throw new ArgumentNullException("refresher"); + + var serversA = servers.ToArray(); + var idsA = ids == null ? null : ids.ToArray(); + + // deliver local + DeliverLocal(refresher, messageType, idsA, json); + + // distribute? + if (RequiresDistributed(serversA, refresher, messageType) == false) + return; + + // deliver remote + DeliverRemote(serversA, refresher, messageType, idsA, json); + } + + protected virtual void Deliver(IEnumerable servers, ICacheRefresher refresher, MessageType messageType, Func getId, IEnumerable instances) + { + if (servers == null) throw new ArgumentNullException("servers"); + if (refresher == null) throw new ArgumentNullException("refresher"); + + var serversA = servers.ToArray(); + var instancesA = instances.ToArray(); + + // deliver local + DeliverLocal(refresher, messageType, getId, instancesA); + + // distribute? + if (RequiresDistributed(serversA, refresher, messageType) == false) + return; + + // deliver remote + + // map ByInstance to ById as there's no remote instances + if (messageType == MessageType.RefreshByInstance) messageType = MessageType.RefreshById; + if (messageType == MessageType.RemoveByInstance) messageType = MessageType.RemoveById; + + // convert instances to identifiers + var idsA = instancesA.Select(getId).ToArray(); + + DeliverRemote(serversA, refresher, messageType, idsA); + } + + //protected virtual void Deliver(IEnumerable servers, ICacheRefresher refresher, object payload) + //{ + // if (servers == null) throw new ArgumentNullException("servers"); + // if (refresher == null) throw new ArgumentNullException("refresher"); + + // var serversA = servers.ToArray(); + + // // deliver local + // DeliverLocal(refresher, payload); + + // // distribute? + // if (RequiresDistributed(serversA, refresher, messageType) == false) + // return; + + // // deliver remote + // DeliverRemote(serversA, refresher, payload); + //} + + #endregion + } +} diff --git a/src/Umbraco.Core/Sync/ServerMessengerResolver.cs b/src/Umbraco.Core/Sync/ServerMessengerResolver.cs index 549f3520e0..b508d18f16 100644 --- a/src/Umbraco.Core/Sync/ServerMessengerResolver.cs +++ b/src/Umbraco.Core/Sync/ServerMessengerResolver.cs @@ -3,24 +3,31 @@ namespace Umbraco.Core.Sync { /// - /// A resolver to return the currently registered IServerMessenger object + /// Resolves the IServerMessenger object. /// public sealed class ServerMessengerResolver : SingleObjectResolverBase { + /// + /// Initializes a new instance of the class with a messenger. + /// + /// An instance of a messenger. + /// The resolver is created by the CoreBootManager and thus the constructor remains internal. internal ServerMessengerResolver(IServerMessenger factory) : base(factory) - { - } + { } /// - /// Can be used at runtime to set a custom IServerMessenger at app startup + /// Sets the messenger. /// - /// + /// The messenger. public void SetServerMessenger(IServerMessenger serverMessenger) { Value = serverMessenger; } + /// + /// Gets the messenger. + /// public IServerMessenger Messenger { get { return Value; } diff --git a/src/Umbraco.Core/Sync/ServerRegistrarResolver.cs b/src/Umbraco.Core/Sync/ServerRegistrarResolver.cs index 196c6fb74c..595dfc0ceb 100644 --- a/src/Umbraco.Core/Sync/ServerRegistrarResolver.cs +++ b/src/Umbraco.Core/Sync/ServerRegistrarResolver.cs @@ -3,25 +3,32 @@ namespace Umbraco.Core.Sync { /// - /// The resolver to return the currently registered IServerRegistrar object + /// Resolves the IServerRegistrar object. /// public sealed class ServerRegistrarResolver : SingleObjectResolverBase { - + /// + /// Initializes a new instance of the class with a registrar. + /// + /// An instance of a registrar. + /// The resolver is created by the CoreBootManager and thus the constructor remains internal. internal ServerRegistrarResolver(IServerRegistrar factory) : base(factory) - { - } + { } /// - /// Can be used at runtime to set a custom IServerRegistrar at app startup + /// Sets the registrar. /// - /// + /// The registrar. + /// For developers, at application startup. public void SetServerRegistrar(IServerRegistrar serverRegistrar) { Value = serverRegistrar; } + /// + /// Gets the registrar. + /// public IServerRegistrar Registrar { get { return Value; } diff --git a/src/Umbraco.Core/Sync/WebServiceServerMessenger.cs b/src/Umbraco.Core/Sync/WebServiceServerMessenger.cs new file mode 100644 index 0000000000..c4b3c03d2f --- /dev/null +++ b/src/Umbraco.Core/Sync/WebServiceServerMessenger.cs @@ -0,0 +1,384 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Threading; +using Newtonsoft.Json; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; +using umbraco.interfaces; + +namespace Umbraco.Core.Sync +{ + /// + /// An that works by messaging servers via web services. + /// + // + // this messenger sends ALL instructions to ALL servers, including the local server. + // the CacheRefresher web service will run ALL instructions, so there may be duplicated, + // except for "bulk" refresh, where it excludes those coming from the local server + // + // TODO see Message() method: stop sending to local server! + // just need to figure out WebServerUtility permissions issues, if any + // + internal class WebServiceServerMessenger : ServerMessengerBase + { + private readonly Func> _getLoginAndPassword; + private volatile bool _hasLoginAndPassword; + private readonly object _locker = new object(); + + protected string Login { get; private set; } + protected string Password{ get; private set; } + + /// + /// Initializes a new instance of the class. + /// + /// Distribution is disabled. + internal WebServiceServerMessenger() + : base(false) + { } + + /// + /// Initializes a new instance of the class with a login and a password. + /// + /// The login. + /// The password. + /// Distribution will be enabled based on the umbraco config setting. + internal WebServiceServerMessenger(string login, string password) + : this(login, password, UmbracoConfig.For.UmbracoSettings().DistributedCall.Enabled) + { + } + + /// + /// Initializes a new instance of the class with a login and a password + /// and a value indicating whether distribution is enabled. + /// + /// The login. + /// The password. + /// A value indicating whether distribution is enabled. + internal WebServiceServerMessenger(string login, string password, bool distributedEnabled) + : base(distributedEnabled) + { + if (login == null) throw new ArgumentNullException("login"); + if (password == null) throw new ArgumentNullException("password"); + + Login = login; + Password = password; + } + + /// + /// Initializes a new instance of the with a function providing + /// a login and a password. + /// + /// A function providing a login and a password. + /// Distribution will be enabled based on the umbraco config setting. + public WebServiceServerMessenger(Func> getLoginAndPassword) + : base(false) // value will be overriden by EnsureUserAndPassword + { + _getLoginAndPassword = getLoginAndPassword; + } + + // lazy-get the login, password, and distributed setting + protected void EnsureLoginAndPassword() + { + if (_hasLoginAndPassword || _getLoginAndPassword == null) return; + + lock (_locker) + { + if (_hasLoginAndPassword) return; + _hasLoginAndPassword = true; + + try + { + var result = _getLoginAndPassword(); + if (result == null) + { + Login = null; + Password = null; + DistributedEnabled = false; + } + else + { + Login = result.Item1; + Password = result.Item2; + DistributedEnabled = UmbracoConfig.For.UmbracoSettings().DistributedCall.Enabled; + } + } + catch (Exception ex) + { + LogHelper.Error("Could not resolve username/password delegate, server distribution will be disabled", ex); + Login = null; + Password = null; + DistributedEnabled = false; + } + } + } + + // this exists only for legacy reasons - we should just pass the server identity un-hashed + public static string GetCurrentServerHash() + { + if (SystemUtilities.GetCurrentTrustLevel() != System.Web.AspNetHostingPermissionLevel.Unrestricted) + throw new NotSupportedException("FullTrust ASP.NET permission level is required."); + return GetServerHash(NetworkHelper.MachineName, System.Web.HttpRuntime.AppDomainAppId); + } + + public static string GetServerHash(string machineName, string appDomainAppId) + { + var hasher = new HashCodeCombiner(); + hasher.AddCaseInsensitiveString(NetworkHelper.MachineName); + hasher.AddCaseInsensitiveString(System.Web.HttpRuntime.AppDomainAppId); + return hasher.GetCombinedHashCode(); + } + + protected override bool RequiresDistributed(IEnumerable servers, ICacheRefresher refresher, MessageType messageType) + { + EnsureLoginAndPassword(); + return base.RequiresDistributed(servers, refresher, messageType); + } + + protected override void DeliverRemote(IEnumerable servers, ICacheRefresher refresher, MessageType messageType, IEnumerable ids = null, string json = null) + { + var idsA = ids == null ? null : ids.ToArray(); + + Type arrayType; + if (GetArrayType(idsA, out arrayType) == false) + throw new ArgumentException("All items must be of the same type, either int or Guid.", "ids"); + + Message(servers, refresher, messageType, idsA, arrayType, json); + } + + protected virtual void Message( + IEnumerable servers, + ICacheRefresher refresher, + MessageType messageType, + IEnumerable ids = null, + Type idArrayType = null, + string jsonPayload = null) + { + LogHelper.Debug( + "Performing distributed call for {0}/{1} on servers ({2}), ids: {3}, json: {4}", + refresher.GetType, + () => messageType, + () => string.Join(";", servers.Select(x => x.ToString())), + () => ids == null ? "" : string.Join(";", ids.Select(x => x.ToString())), + () => jsonPayload ?? ""); + + try + { + // NOTE: we are messaging ALL servers including the local server + // at the moment, the web service, + // for bulk (batched) checks the origin and does NOT process the instructions again + // for anything else, processes the instructions again (but we don't use this anymore, batched is the default) + // TODO: see WebServerHelper, could remove local server from the list of servers + + // the default server messenger uses http requests + using (var client = new ServerSyncWebServiceClient()) + { + var asyncResults = new List(); + + LogStartDispatch(); + + // go through each configured node submitting a request asynchronously + // NOTE: 'asynchronously' in this case does not mean that it will continue while we give the page back to the user! + foreach (var n in servers) + { + // set the server address + client.Url = n.ServerAddress; + + // add the returned WaitHandle to the list for later checking + switch (messageType) + { + case MessageType.RefreshByJson: + asyncResults.Add(client.BeginRefreshByJson(refresher.UniqueIdentifier, jsonPayload, Login, Password, null, null)); + break; + + case MessageType.RefreshAll: + asyncResults.Add(client.BeginRefreshAll(refresher.UniqueIdentifier, Login, Password, null, null)); + break; + + case MessageType.RefreshById: + if (idArrayType == null) + throw new InvalidOperationException("Cannot refresh by id if the idArrayType is null."); + + if (idArrayType == typeof(int)) + { + // bulk of ints is supported + var json = JsonConvert.SerializeObject(ids.Cast().ToArray()); + var result = client.BeginRefreshByIds(refresher.UniqueIdentifier, json, Login, Password, null, null); + asyncResults.Add(result); + } + else // must be guids + { + // bulk of guids is not supported, iterate + asyncResults.AddRange(ids.Select(i => + client.BeginRefreshByGuid(refresher.UniqueIdentifier, (Guid)i, Login, Password, null, null))); + } + + break; + case MessageType.RemoveById: + if (idArrayType == null) + throw new InvalidOperationException("Cannot remove by id if the idArrayType is null."); + + // must be ints + asyncResults.AddRange(ids.Select(i => + client.BeginRemoveById(refresher.UniqueIdentifier, (int)i, Login, Password, null, null))); + break; + } + } + + // wait for all requests to complete + var waitHandles = asyncResults.Select(x => x.AsyncWaitHandle); + WaitHandle.WaitAll(waitHandles.ToArray()); + + // handle results + var errorCount = 0; + foreach (var asyncResult in asyncResults) + { + try + { + switch (messageType) + { + case MessageType.RefreshByJson: + client.EndRefreshByJson(asyncResult); + break; + + case MessageType.RefreshAll: + client.EndRefreshAll(asyncResult); + break; + + case MessageType.RefreshById: + if (idArrayType == typeof(int)) + client.EndRefreshById(asyncResult); + else + client.EndRefreshByGuid(asyncResult); + break; + + case MessageType.RemoveById: + client.EndRemoveById(asyncResult); + break; + } + } + catch (WebException ex) + { + LogDispatchNodeError(ex); + errorCount++; + } + catch (Exception ex) + { + LogDispatchNodeError(ex); + errorCount++; + } + } + + LogDispatchBatchResult(errorCount); + } + } + catch (Exception ee) + { + LogDispatchBatchError(ee); + } + } + + protected virtual void Message(IEnumerable envelopes) + { + var envelopesA = envelopes.ToArray(); + var servers = envelopesA.SelectMany(x => x.Servers).Distinct(); + + try + { + // NOTE: we are messaging ALL servers including the local server + // at the moment, the web service, + // for bulk (batched) checks the origin and does NOT process the instructions again + // for anything else, processes the instructions again (but we don't use this anymore, batched is the default) + // TODO: see WebServerHelper, could remove local server from the list of servers + + using (var client = new ServerSyncWebServiceClient()) + { + var asyncResults = new List(); + + LogStartDispatch(); + + // go through each configured node submitting a request asynchronously + // NOTE: 'asynchronously' in this case does not mean that it will continue while we give the page back to the user! + foreach (var server in servers) + { + // set the server address + client.Url = server.ServerAddress; + + var serverInstructions = envelopesA + .Where(x => x.Servers.Contains(server)) + .SelectMany(x => x.Instructions) + .Distinct() // only execute distinct instructions - no sense in running the same one. + .ToArray(); + + asyncResults.Add( + client.BeginBulkRefresh( + serverInstructions, + GetCurrentServerHash(), + Login, Password, null, null)); + } + + // wait for all requests to complete + var waitHandles = asyncResults.Select(x => x.AsyncWaitHandle).ToArray(); + WaitHandle.WaitAll(waitHandles.ToArray()); + + // handle results + var errorCount = 0; + foreach (var asyncResult in asyncResults) + { + try + { + client.EndBulkRefresh(asyncResult); + } + catch (WebException ex) + { + LogDispatchNodeError(ex); + errorCount++; + } + catch (Exception ex) + { + LogDispatchNodeError(ex); + errorCount++; + } + } + LogDispatchBatchResult(errorCount); + } + } + catch (Exception ee) + { + LogDispatchBatchError(ee); + } + } + + #region Logging + + private static void LogDispatchBatchError(Exception ee) + { + LogHelper.Error("Error refreshing distributed list", ee); + } + + private static void LogDispatchBatchResult(int errorCount) + { + LogHelper.Debug(string.Format("Distributed server push completed with {0} nodes reporting an error", errorCount == 0 ? "no" : errorCount.ToString(CultureInfo.InvariantCulture))); + } + + private static void LogDispatchNodeError(Exception ex) + { + LogHelper.Error("Error refreshing a node in the distributed list", ex); + } + + private static void LogDispatchNodeError(WebException ex) + { + string url = (ex.Response != null) ? ex.Response.ResponseUri.ToString() : "invalid url (responseUri null)"; + LogHelper.Error("Error refreshing a node in the distributed list, URI attempted: " + url, ex); + } + + private static void LogStartDispatch() + { + LogHelper.Info("Submitting calls to distributed servers"); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 947401b799..486772360a 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -385,9 +385,11 @@ + + @@ -457,6 +459,7 @@ + @@ -1174,15 +1177,23 @@ + + + + + + + + - + Component diff --git a/src/Umbraco.Tests/Cache/CacheRefresherTests.cs b/src/Umbraco.Tests/Cache/CacheRefresherTests.cs index c5be44dcdd..690ad80525 100644 --- a/src/Umbraco.Tests/Cache/CacheRefresherTests.cs +++ b/src/Umbraco.Tests/Cache/CacheRefresherTests.cs @@ -1,4 +1,5 @@ using NUnit.Framework; +using Umbraco.Core.Sync; using umbraco.presentation.webservices; namespace Umbraco.Tests.Cache @@ -13,8 +14,10 @@ namespace Umbraco.Tests.Cache [TestCase("fffffff28449cf3", "123456", "testmachine", true)] public void Continue_Refreshing_For_Request(string hash, string appDomainAppId, string machineName, bool expected) { - var refresher = new CacheRefresher(); - Assert.AreEqual(expected, refresher.ContinueRefreshingForRequest(hash, appDomainAppId, machineName)); + if (expected) + Assert.AreEqual(hash, WebServiceServerMessenger.GetServerHash(appDomainAppId, machineName)); + else + Assert.AreNotEqual(hash, WebServiceServerMessenger.GetServerHash(appDomainAppId, machineName)); } } diff --git a/src/Umbraco.Tests/Persistence/PetaPocoExtensionsTest.cs b/src/Umbraco.Tests/Persistence/PetaPocoExtensionsTest.cs index 9a5b994522..463657c4bd 100644 --- a/src/Umbraco.Tests/Persistence/PetaPocoExtensionsTest.cs +++ b/src/Umbraco.Tests/Persistence/PetaPocoExtensionsTest.cs @@ -223,11 +223,11 @@ namespace Umbraco.Tests.Persistence { servers.Add(new ServerRegistrationDto { - Address = "address" + i, - ComputerName = "computer" + i, + ServerAddress = "address" + i, + ServerIdentity = "computer" + i, DateRegistered = DateTime.Now, IsActive = true, - LastNotified = DateTime.Now + DateAccessed = DateTime.Now }); } @@ -252,11 +252,11 @@ namespace Umbraco.Tests.Persistence { servers.Add(new ServerRegistrationDto { - Address = "address" + i, - ComputerName = "computer" + i, + ServerAddress = "address" + i, + ServerIdentity = "computer" + i, DateRegistered = DateTime.Now, IsActive = true, - LastNotified = DateTime.Now + DateAccessed = DateTime.Now }); } db.OpenSharedConnection(); @@ -283,11 +283,11 @@ namespace Umbraco.Tests.Persistence { servers.Add(new ServerRegistrationDto { - Address = "address" + i, - ComputerName = "computer" + i, + ServerAddress = "address" + i, + ServerIdentity = "computer" + i, DateRegistered = DateTime.Now, IsActive = true, - LastNotified = DateTime.Now + DateAccessed = DateTime.Now }); } db.OpenSharedConnection(); diff --git a/src/Umbraco.Tests/Persistence/Repositories/ServerRegistrationRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ServerRegistrationRepositoryTest.cs index 9433c9a2b6..c32e214177 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ServerRegistrationRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ServerRegistrationRepositoryTest.cs @@ -32,7 +32,7 @@ namespace Umbraco.Tests.Persistence.Repositories } [Test] - public void Cannot_Add_Duplicate_Computer_Names() + public void Cannot_Add_Duplicate_Server_Identities() { // Arrange var provider = new PetaPocoUnitOfWorkProvider(Logger); @@ -50,7 +50,7 @@ namespace Umbraco.Tests.Persistence.Repositories } [Test] - public void Cannot_Update_To_Duplicate_Computer_Names() + public void Cannot_Update_To_Duplicate_Server_Identities() { // Arrange var provider = new PetaPocoUnitOfWorkProvider(Logger); @@ -60,7 +60,7 @@ namespace Umbraco.Tests.Persistence.Repositories using (var repository = CreateRepositor(unitOfWork)) { var server = repository.Get(1); - server.ComputerName = "COMPUTER2"; + server.ServerIdentity = "COMPUTER2"; repository.AddOrUpdate(server); Assert.Throws(unitOfWork.Commit); } @@ -128,7 +128,7 @@ namespace Umbraco.Tests.Persistence.Repositories using (var repository = CreateRepositor(unitOfWork)) { // Act - var query = Query.Builder.Where(x => x.ComputerName.ToUpper() == "COMPUTER3"); + var query = Query.Builder.Where(x => x.ServerIdentity.ToUpper() == "COMPUTER3"); var result = repository.GetByQuery(query); // Assert diff --git a/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs b/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs new file mode 100644 index 0000000000..f98138c7f3 --- /dev/null +++ b/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Web; +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Core.Sync; +using Umbraco.Web.Routing; + +namespace Umbraco.Web +{ + public class BatchedDatabaseServerMessenger : Core.Sync.BatchedDatabaseServerMessenger + { + public BatchedDatabaseServerMessenger(ApplicationContext appContext, bool enableDistCalls, DatabaseServerMessengerOptions options) + : base(appContext, enableDistCalls, options, GetBatch) + { + UmbracoApplicationBase.ApplicationStarted += Application_Started; + UmbracoModule.EndRequest += UmbracoModule_EndRequest; + UmbracoModule.RouteAttempt += UmbracoModule_RouteAttempt; + } + + private void Application_Started(object sender, EventArgs eventArgs) + { + if (ApplicationContext.IsConfigured == false + || ApplicationContext.DatabaseContext.IsDatabaseConfigured == false + || ApplicationContext.DatabaseContext.CanConnect == false) + + LogHelper.Warn("The app is not configured or cannot connect to the database, this server cannot be initialized with " + + typeof(BatchedDatabaseServerMessenger) + ", distributed calls will not be enabled for this server"); + + // because .ApplicationStarted triggers only once, this is thread-safe + Boot(); + } + + private void UmbracoModule_RouteAttempt(object sender, RoutableAttemptEventArgs e) + { + switch (e.Outcome) + { + case EnsureRoutableOutcome.IsRoutable: + Sync(); + break; + case EnsureRoutableOutcome.NotDocumentRequest: + //so it's not a document request, we'll check if it's a back office request + if (e.HttpContext.Request.Url.IsBackOfficeRequest(HttpRuntime.AppDomainAppVirtualPath)) + { + //it's a back office request, we should sync! + Sync(); + } + break; + //case EnsureRoutableOutcome.NotReady: + //case EnsureRoutableOutcome.NotConfigured: + //case EnsureRoutableOutcome.NoContent: + //default: + // break; + } + } + + private void UmbracoModule_EndRequest(object sender, EventArgs e) + { + // will clear the batch - will remain in HttpContext though - that's ok + FlushBatch(); + } + + private static ICollection GetBatch(bool ensure) + { + var httpContext = UmbracoContext.Current == null ? null : UmbracoContext.Current.HttpContext; + if (httpContext == null) + throw new NotSupportedException("Cannot execute without a valid/current UmbracoContext with an HttpContext assigned."); + + var key = typeof (BatchedDatabaseServerMessenger).Name; + + // no thread-safety here because it'll run in only 1 thread (request) at a time + var batch = (ICollection)httpContext.Items[key]; + if (batch == null && ensure) + httpContext.Items[key] = batch = new List(); + return batch; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/BatchedServerMessenger.cs b/src/Umbraco.Web/BatchedServerMessenger.cs deleted file mode 100644 index 65de928e56..0000000000 --- a/src/Umbraco.Web/BatchedServerMessenger.cs +++ /dev/null @@ -1,326 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Net; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using System.Web; -using System.Web.Script.Serialization; -using System.Web.UI.WebControls; -using Umbraco.Core; -using Umbraco.Core.Cache; -using Umbraco.Core.Logging; -using Umbraco.Core.Sync; -using umbraco.interfaces; - -namespace Umbraco.Web -{ - internal class BatchedServerMessenger : DefaultServerMessenger - { - internal BatchedServerMessenger() - { - UmbracoModule.EndRequest += UmbracoModule_EndRequest; - } - - internal BatchedServerMessenger(string login, string password) : base(login, password) - { - UmbracoModule.EndRequest += UmbracoModule_EndRequest; - } - - internal BatchedServerMessenger(string login, string password, bool useDistributedCalls) : base(login, password, useDistributedCalls) - { - UmbracoModule.EndRequest += UmbracoModule_EndRequest; - } - - public BatchedServerMessenger(Func> getUserNamePasswordDelegate) : base(getUserNamePasswordDelegate) - { - UmbracoModule.EndRequest += UmbracoModule_EndRequest; - } - - void UmbracoModule_EndRequest(object sender, EventArgs e) - { - if (HttpContext.Current == null) - { - return; - } - - var items = HttpContext.Current.Items[typeof(BatchedServerMessenger).Name] as List; - if (items != null) - { - var copied = new Message[items.Count]; - items.CopyTo(copied); - //now set to null so it get's cleaned up on this request - HttpContext.Current.Items[typeof (BatchedServerMessenger).Name] = null; - - SendMessages(copied); - } - } - - private class Message - { - public IEnumerable Servers { get; set; } - public ICacheRefresher Refresher { get; set; } - public MessageType DispatchType { get; set; } - public IEnumerable Ids { get; set; } - public Type IdArrayType { get; set; } - public string JsonPayload { get; set; } - } - - /// - /// We need to check if distributed calls are enabled, if they are we also want to make sure - /// that the current server's cache is updated internally in real time instead of at the end of - /// the call. This is because things like the URL cache, etc... might need to be updated during - /// the request that is making these calls. - /// - /// - /// - /// - /// - /// - /// - /// See: http://issues.umbraco.org/issue/U4-2633#comment=67-15604 - /// - protected override void MessageSeversForIdsOrJson(IEnumerable servers, ICacheRefresher refresher, MessageType dispatchType, IEnumerable ids = null, string jsonPayload = null) - { - //do all the normal stuff - base.MessageSeversForIdsOrJson(servers, refresher, dispatchType, ids, jsonPayload); - - //Now, check if we are using Distrubuted calls - if (UseDistributedCalls && servers.Any()) - { - //invoke on the current server - we will basically be double cache refreshing for the calling - // server but that just needs to be done currently, see the link above for details. - InvokeMethodOnRefresherInstance(refresher, dispatchType, ids, jsonPayload); - } - } - - /// - /// This adds the call to batched list - /// - /// - /// - /// - /// - /// - /// - protected override void PerformDistributedCall( - IEnumerable servers, - ICacheRefresher refresher, - MessageType dispatchType, - IEnumerable ids = null, - Type idArrayType = null, - string jsonPayload = null) - { - - //NOTE: we use UmbracoContext instead of HttpContext.Current because when some web methods run async, the - // HttpContext.Current is null but the UmbracoContext.Current won't be since we manually assign it. - if (UmbracoContext.Current == null || UmbracoContext.Current.HttpContext == null) - { - throw new NotSupportedException("This messenger cannot execute without a valid/current UmbracoContext with an HttpContext assigned"); - } - - if (UmbracoContext.Current.HttpContext.Items[typeof(BatchedServerMessenger).Name] == null) - { - UmbracoContext.Current.HttpContext.Items[typeof(BatchedServerMessenger).Name] = new List(); - } - var list = (List)UmbracoContext.Current.HttpContext.Items[typeof(BatchedServerMessenger).Name]; - - list.Add(new Message - { - DispatchType = dispatchType, - IdArrayType = idArrayType, - Ids = ids, - JsonPayload = jsonPayload, - Refresher = refresher, - Servers = servers - }); - } - - private RefreshInstruction[] ConvertToInstruction(Message msg) - { - switch (msg.DispatchType) - { - case MessageType.RefreshAll: - return new[] - { - new RefreshInstruction - { - RefreshType = RefreshInstruction.RefreshMethodType.RefreshAll, - RefresherId = msg.Refresher.UniqueIdentifier - } - }; - case MessageType.RefreshById: - if (msg.IdArrayType == null) - { - throw new InvalidOperationException("Cannot refresh by id if the idArrayType is null"); - } - - if (msg.IdArrayType == typeof(int)) - { - var serializer = new JavaScriptSerializer(); - var jsonIds = serializer.Serialize(msg.Ids.Cast().ToArray()); - - return new[] - { - new RefreshInstruction - { - JsonIds = jsonIds, - RefreshType = RefreshInstruction.RefreshMethodType.RefreshByIds, - RefresherId = msg.Refresher.UniqueIdentifier - } - }; - } - - return msg.Ids.Select(x => new RefreshInstruction - { - GuidId = (Guid)x, - RefreshType = RefreshInstruction.RefreshMethodType.RefreshById, - RefresherId = msg.Refresher.UniqueIdentifier - }).ToArray(); - - case MessageType.RefreshByJson: - return new[] - { - new RefreshInstruction - { - RefreshType = RefreshInstruction.RefreshMethodType.RefreshByJson, - RefresherId = msg.Refresher.UniqueIdentifier, - JsonPayload = msg.JsonPayload - } - }; - case MessageType.RemoveById: - return msg.Ids.Select(x => new RefreshInstruction - { - IntId = (int)x, - RefreshType = RefreshInstruction.RefreshMethodType.RemoveById, - RefresherId = msg.Refresher.UniqueIdentifier - }).ToArray(); - case MessageType.RefreshByInstance: - case MessageType.RemoveByInstance: - default: - throw new ArgumentOutOfRangeException(); - } - } - - private void SendMessages(IEnumerable messages) - { - var batchedMsg = new List>(); - foreach (var msg in messages) - { - var instructions = ConvertToInstruction(msg); - batchedMsg.Add(new Tuple(msg, instructions)); - } - - var servers = batchedMsg.SelectMany(x => x.Item1.Servers).Distinct(); - - try - { - - //TODO: We should try to figure out the current server's address and if it matches any of the ones - // in the ServerAddress list, then just refresh directly on this server and exclude that server address - // from the list, this will save an internal request. - - using (var cacheRefresher = new ServerSyncWebServiceClient()) - { - var asyncResultsList = new List(); - - LogStartDispatch(); - - // Go through each configured node submitting a request asynchronously - //NOTE: 'asynchronously' in this case does not mean that it will continue while we give the page back to the user! - foreach (var server in servers) - { - //set the server address - cacheRefresher.Url = server.ServerAddress; - - var instructions = batchedMsg - .Where(x => x.Item1.Servers.Contains(server)) - .SelectMany(x => x.Item2) - //only execute distinct instructions - no sense in running the same one. - .Distinct() - .ToArray(); - - //Create a hash of the server name and the IIS app Id to send up so we don't double cache refresh the - // master server. - //Fixes: http://issues.umbraco.org/issue/U4-5491 - //NOTE: This will only work in full trust, in med trust, a double cache refresh is inevitable - var hashedAppId = string.Empty; - if (SystemUtilities.GetCurrentTrustLevel() == AspNetHostingPermissionLevel.Unrestricted) - { - var hasher = new HashCodeCombiner(); - hasher.AddCaseInsensitiveString(NetworkHelper.MachineName); - hasher.AddCaseInsensitiveString(HttpRuntime.AppDomainAppId); - hashedAppId = hasher.GetCombinedHashCode(); - } - - asyncResultsList.Add( - cacheRefresher.BeginBulkRefresh( - instructions, - hashedAppId, - Login, Password, null, null)); - } - - var waitHandlesList = asyncResultsList.Select(x => x.AsyncWaitHandle).ToArray(); - - var errorCount = 0; - - //Wait for all requests to complete - WaitHandle.WaitAll(waitHandlesList.ToArray()); - - foreach (var t in asyncResultsList) - { - //var handleIndex = WaitHandle.WaitAny(waitHandlesList.ToArray(), TimeSpan.FromSeconds(15)); - - try - { - cacheRefresher.EndBulkRefresh(t); - } - catch (WebException ex) - { - LogDispatchNodeError(ex); - errorCount++; - } - catch (Exception ex) - { - LogDispatchNodeError(ex); - errorCount++; - } - } - LogDispatchBatchResult(errorCount); - } - } - catch (Exception ee) - { - LogDispatchBatchError(ee); - } - } - - private void LogDispatchBatchError(Exception ee) - { - LogHelper.Error("Error refreshing distributed list", ee); - } - - private void LogDispatchBatchResult(int errorCount) - { - LogHelper.Debug(string.Format("Distributed server push completed with {0} nodes reporting an error", errorCount == 0 ? "no" : errorCount.ToString(CultureInfo.InvariantCulture))); - } - - private void LogDispatchNodeError(Exception ex) - { - LogHelper.Error("Error refreshing a node in the distributed list", ex); - } - - private void LogDispatchNodeError(WebException ex) - { - string url = (ex.Response != null) ? ex.Response.ResponseUri.ToString() : "invalid url (responseUri null)"; - LogHelper.Error("Error refreshing a node in the distributed list, URI attempted: " + url, ex); - } - - private void LogStartDispatch() - { - LogHelper.Info("Submitting calls to distributed servers"); - } - } -} diff --git a/src/Umbraco.Web/BatchedWebServiceServerMessenger.cs b/src/Umbraco.Web/BatchedWebServiceServerMessenger.cs new file mode 100644 index 0000000000..1789f92d9a --- /dev/null +++ b/src/Umbraco.Web/BatchedWebServiceServerMessenger.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using Umbraco.Core.Sync; + +namespace Umbraco.Web +{ + internal class BatchedWebServiceServerMessenger : Core.Sync.BatchedWebServiceServerMessenger + { + internal BatchedWebServiceServerMessenger() + : base(GetBatch) + { + UmbracoModule.EndRequest += UmbracoModule_EndRequest; + } + + internal BatchedWebServiceServerMessenger(string login, string password) + : base(login, password, GetBatch) + { + UmbracoModule.EndRequest += UmbracoModule_EndRequest; + } + + internal BatchedWebServiceServerMessenger(string login, string password, bool useDistributedCalls) + : base(login, password, useDistributedCalls, GetBatch) + { + UmbracoModule.EndRequest += UmbracoModule_EndRequest; + } + + public BatchedWebServiceServerMessenger(Func> getLoginAndPassword) + : base(getLoginAndPassword, GetBatch) + { + UmbracoModule.EndRequest += UmbracoModule_EndRequest; + } + + private static ICollection GetBatch(bool ensure) + { + var httpContext = UmbracoContext.Current == null ? null : UmbracoContext.Current.HttpContext; + if (httpContext == null) + throw new NotSupportedException("Cannot execute without a valid/current UmbracoContext with an HttpContext assigned."); + + var key = typeof(BatchedWebServiceServerMessenger).Name; + + // no thread-safety here because it'll run in only 1 thread (request) at a time + var batch = (ICollection)httpContext.Items[key]; + if (batch == null && ensure) + httpContext.Items[key] = batch = new List(); + return batch; + } + + void UmbracoModule_EndRequest(object sender, EventArgs e) + { + FlushBatch(); + } + + protected override void ProcessBatch(RefreshInstructionEnvelope[] batch) + { + Message(batch); + } + } +} diff --git a/src/Umbraco.Web/Cache/DistributedCache.cs b/src/Umbraco.Web/Cache/DistributedCache.cs index bb0d4821b8..db67600757 100644 --- a/src/Umbraco.Web/Cache/DistributedCache.cs +++ b/src/Umbraco.Web/Cache/DistributedCache.cs @@ -1,38 +1,20 @@ using System; -using System.Collections.Generic; -using System.Globalization; using System.Linq; -using System.Net; -using System.Threading; -using System.Web.Services.Protocols; -using System.Xml; using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.IO; -using Umbraco.Core.Logging; using Umbraco.Core.Sync; -using umbraco.BusinessLogic; using umbraco.interfaces; namespace Umbraco.Web.Cache { - //public class CacheUpdatedEventArgs : EventArgs - //{ - - //} - /// - /// DistributedCache is used to invalidate cache throughout the application which also takes in to account load balancing environments automatically + /// Represents the entry point into Umbraco's distributed cache infrastructure. /// /// - /// Distributing calls to all registered load balanced servers, ensuring that content are synced and cached on all servers. - /// Dispatcher is exendable, so 3rd party services can easily be integrated into the workflow, using the interfaces.ICacheRefresher interface. - /// - /// Dispatcher can refresh/remove content, templates and macros. + /// The distributed cache infrastructure ensures that distributed caches are + /// invalidated properly in load balancing environments. /// public sealed class DistributedCache { - #region Public constants/Ids public const string ApplicationTreeCacheRefresherId = "0AC6C028-9860-4EA4-958D-14D39F45886E"; @@ -56,21 +38,45 @@ namespace Umbraco.Web.Cache public const string DictionaryCacheRefresherId = "D1D7E227-F817-4816-BFE9-6C39B6152884"; public const string PublicAccessCacheRefresherId = "1DB08769-B104-4F8B-850E-169CAC1DF2EC"; + public static readonly Guid ApplicationTreeCacheRefresherGuid = new Guid(ApplicationTreeCacheRefresherId); + public static readonly Guid ApplicationCacheRefresherGuid = new Guid(ApplicationCacheRefresherId); + public static readonly Guid TemplateRefresherGuid = new Guid(TemplateRefresherId); + public static readonly Guid PageCacheRefresherGuid = new Guid(PageCacheRefresherId); + public static readonly Guid UnpublishedPageCacheRefresherGuid = new Guid(UnpublishedPageCacheRefresherId); + public static readonly Guid MemberCacheRefresherGuid = new Guid(MemberCacheRefresherId); + public static readonly Guid MemberGroupCacheRefresherGuid = new Guid(MemberGroupCacheRefresherId); + public static readonly Guid MediaCacheRefresherGuid = new Guid(MediaCacheRefresherId); + public static readonly Guid MacroCacheRefresherGuid = new Guid(MacroCacheRefresherId); + public static readonly Guid UserCacheRefresherGuid = new Guid(UserCacheRefresherId); + public static readonly Guid UserPermissionsCacheRefresherGuid = new Guid(UserPermissionsCacheRefresherId); + public static readonly Guid UserTypeCacheRefresherGuid = new Guid(UserTypeCacheRefresherId); + public static readonly Guid ContentTypeCacheRefresherGuid = new Guid(ContentTypeCacheRefresherId); + public static readonly Guid LanguageCacheRefresherGuid = new Guid(LanguageCacheRefresherId); + public static readonly Guid DomainCacheRefresherGuid = new Guid(DomainCacheRefresherId); + public static readonly Guid StylesheetCacheRefresherGuid = new Guid(StylesheetCacheRefresherId); + public static readonly Guid StylesheetPropertyCacheRefresherGuid = new Guid(StylesheetPropertyCacheRefresherId); + public static readonly Guid DataTypeCacheRefresherGuid = new Guid(DataTypeCacheRefresherId); + public static readonly Guid DictionaryCacheRefresherGuid = new Guid(DictionaryCacheRefresherId); + public static readonly Guid PublicAccessCacheRefresherGuid = new Guid(PublicAccessCacheRefresherId); + #endregion + #region Constructor & Singleton + + // note - should inject into the application instead of using a singleton private static readonly DistributedCache InstanceObject = new DistributedCache(); /// - /// Constructor + /// Initializes a new instance of the class. /// private DistributedCache() - { - } + { } /// - /// Singleton + /// Gets the static unique instance of the class. /// - /// + /// The static unique instance of the class. + /// Exists so that extension methods can be added to the distributed cache. public static DistributedCache Instance { get @@ -79,22 +85,25 @@ namespace Umbraco.Web.Cache } } + #endregion + + #region Core notification methods + /// - /// Sends a request to all registered load-balanced servers to refresh node with the specified Id - /// using the specified ICacheRefresher with the guid factoryGuid. + /// Notifies the distributed cache of specifieds item invalidation, for a specified . /// - /// - /// - /// The callback method to retrieve the ID from an instance - /// The instances containing Ids + /// The type of the invalidated items. + /// The unique identifier of the ICacheRefresher. + /// A function returning the unique identifier of items. + /// The invalidated items. /// - /// This method is much better for performance because it does not need to re-lookup an object instance + /// This method is much better for performance because it does not need to re-lookup object instances. /// public void Refresh(Guid factoryGuid, Func getNumericId, params T[] instances) { if (factoryGuid == Guid.Empty || instances.Length == 0 || getNumericId == null) return; - ServerMessengerResolver.Current.Messenger.PerformRefresh( + ServerMessengerResolver.Current.Messenger.PerformRefresh( ServerRegistrarResolver.Current.Registrar.Registrations, GetRefresherById(factoryGuid), getNumericId, @@ -102,11 +111,10 @@ namespace Umbraco.Web.Cache } /// - /// Sends a request to all registered load-balanced servers to refresh node with the specified Id - /// using the specified ICacheRefresher with the guid factoryGuid. + /// Notifies the distributed cache of a specified item invalidation, for a specified . /// - /// The unique identifier of the ICacheRefresher used to refresh the node. - /// The id of the node. + /// The unique identifier of the ICacheRefresher. + /// The unique identifier of the invalidated item. public void Refresh(Guid factoryGuid, int id) { if (factoryGuid == Guid.Empty || id == default(int)) return; @@ -118,11 +126,10 @@ namespace Umbraco.Web.Cache } /// - /// Sends a request to all registered load-balanced servers to refresh the node with the specified guid - /// using the specified ICacheRefresher with the guid factoryGuid. + /// Notifies the distributed cache of a specified item invalidation, for a specified . /// - /// The unique identifier of the ICacheRefresher used to refresh the node. - /// The guid of the node. + /// The unique identifier of the ICacheRefresher. + /// The unique identifier of the invalidated item. public void Refresh(Guid factoryGuid, Guid id) { if (factoryGuid == Guid.Empty || id == Guid.Empty) return; @@ -134,11 +141,10 @@ namespace Umbraco.Web.Cache } /// - /// Sends a request to all registered load-balanced servers to refresh data based on the custom json payload - /// using the specified ICacheRefresher with the guid factoryGuid. + /// Notifies the distributed cache, for a specified . /// - /// - /// + /// The unique identifier of the ICacheRefresher. + /// The notification content. public void RefreshByJson(Guid factoryGuid, string jsonPayload) { if (factoryGuid == Guid.Empty || jsonPayload.IsNullOrWhiteSpace()) return; @@ -149,26 +155,37 @@ namespace Umbraco.Web.Cache jsonPayload); } + ///// + ///// Notifies the distributed cache, for a specified . + ///// + ///// The unique identifier of the ICacheRefresher. + ///// The notification content. + //internal void Notify(Guid refresherId, object payload) + //{ + // if (refresherId == Guid.Empty || payload == null) return; + + // ServerMessengerResolver.Current.Messenger.Notify( + // ServerRegistrarResolver.Current.Registrar.Registrations, + // GetRefresherById(refresherId), + // json); + //} + /// - /// Sends a request to all registered load-balanced servers to refresh all nodes - /// using the specified ICacheRefresher with the guid factoryGuid. + /// Notifies the distributed cache of a global invalidation for a specified . /// - /// The unique identifier. + /// The unique identifier of the ICacheRefresher. public void RefreshAll(Guid factoryGuid) { if (factoryGuid == Guid.Empty) return; - RefreshAll(factoryGuid, true); } /// - /// Sends a request to all registered load-balanced servers to refresh all nodes - /// using the specified ICacheRefresher with the guid factoryGuid. + /// Notifies the distributed cache of a global invalidation for a specified . /// - /// The unique identifier. - /// - /// If true will send the request out to all registered LB servers, if false will only execute the current server - /// + /// The unique identifier of the ICacheRefresher. + /// If true, all servers in the load balancing environment are notified; otherwise, + /// only the local server is notified. public void RefreshAll(Guid factoryGuid, bool allServers) { if (factoryGuid == Guid.Empty) return; @@ -181,11 +198,10 @@ namespace Umbraco.Web.Cache } /// - /// Sends a request to all registered load-balanced servers to remove the node with the specified id - /// using the specified ICacheRefresher with the guid factoryGuid. + /// Notifies the distributed cache of a specified item removal, for a specified . /// - /// The unique identifier. - /// The id. + /// The unique identifier of the ICacheRefresher. + /// The unique identifier of the removed item. public void Remove(Guid factoryGuid, int id) { if (factoryGuid == Guid.Empty || id == default(int)) return; @@ -195,28 +211,32 @@ namespace Umbraco.Web.Cache GetRefresherById(factoryGuid), id); } - + /// - /// Sends a request to all registered load-balanced servers to remove the node specified - /// using the specified ICacheRefresher with the guid factoryGuid. + /// Notifies the distributed cache of specifieds item removal, for a specified . /// - /// - /// - /// - /// + /// The type of the removed items. + /// The unique identifier of the ICacheRefresher. + /// A function returning the unique identifier of items. + /// The removed items. + /// + /// This method is much better for performance because it does not need to re-lookup object instances. + /// public void Remove(Guid factoryGuid, Func getNumericId, params T[] instances) { - ServerMessengerResolver.Current.Messenger.PerformRemove( + ServerMessengerResolver.Current.Messenger.PerformRemove( ServerRegistrarResolver.Current.Registrar.Registrations, GetRefresherById(factoryGuid), getNumericId, instances); - } + } + #endregion + + // helper method to get an ICacheRefresher by its unique identifier private static ICacheRefresher GetRefresherById(Guid uniqueIdentifier) { return CacheRefreshersResolver.Current.GetById(uniqueIdentifier); } - } } diff --git a/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs b/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs index 3e053fcd18..c3c1d64d89 100644 --- a/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs +++ b/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.Events; @@ -11,7 +9,7 @@ using umbraco.cms.businesslogic.web; namespace Umbraco.Web.Cache { /// - /// Extension methods for DistrubutedCache + /// Extension methods for /// internal static class DistributedCacheExtensions { @@ -19,628 +17,383 @@ namespace Umbraco.Web.Cache public static void RefreshPublicAccess(this DistributedCache dc) { - dc.RefreshAll(new Guid(DistributedCache.PublicAccessCacheRefresherId)); + dc.RefreshAll(DistributedCache.PublicAccessCacheRefresherGuid); } - #endregion #region Application tree cache + public static void RefreshAllApplicationTreeCache(this DistributedCache dc) { - dc.RefreshAll(new Guid(DistributedCache.ApplicationTreeCacheRefresherId)); + dc.RefreshAll(DistributedCache.ApplicationTreeCacheRefresherGuid); } + #endregion #region Application cache + public static void RefreshAllApplicationCache(this DistributedCache dc) { - dc.RefreshAll(new Guid(DistributedCache.ApplicationCacheRefresherId)); + dc.RefreshAll(DistributedCache.ApplicationCacheRefresherGuid); } + #endregion #region User type cache + public static void RemoveUserTypeCache(this DistributedCache dc, int userTypeId) { - dc.Remove(new Guid(DistributedCache.UserTypeCacheRefresherId), userTypeId); + dc.Remove(DistributedCache.UserTypeCacheRefresherGuid, userTypeId); } public static void RefreshUserTypeCache(this DistributedCache dc, int userTypeId) { - dc.Refresh(new Guid(DistributedCache.UserTypeCacheRefresherId), userTypeId); + dc.Refresh(DistributedCache.UserTypeCacheRefresherGuid, userTypeId); } public static void RefreshAllUserTypeCache(this DistributedCache dc) { - dc.RefreshAll(new Guid(DistributedCache.UserTypeCacheRefresherId)); + dc.RefreshAll(DistributedCache.UserTypeCacheRefresherGuid); } + #endregion #region User cache + public static void RemoveUserCache(this DistributedCache dc, int userId) { - dc.Remove(new Guid(DistributedCache.UserCacheRefresherId), userId); + dc.Remove(DistributedCache.UserCacheRefresherGuid, userId); } public static void RefreshUserCache(this DistributedCache dc, int userId) { - dc.Refresh(new Guid(DistributedCache.UserCacheRefresherId), userId); + dc.Refresh(DistributedCache.UserCacheRefresherGuid, userId); } public static void RefreshAllUserCache(this DistributedCache dc) { - dc.RefreshAll(new Guid(DistributedCache.UserCacheRefresherId)); + dc.RefreshAll(DistributedCache.UserCacheRefresherGuid); } + #endregion #region User permissions cache + public static void RemoveUserPermissionsCache(this DistributedCache dc, int userId) { - dc.Remove(new Guid(DistributedCache.UserPermissionsCacheRefresherId), userId); + dc.Remove(DistributedCache.UserPermissionsCacheRefresherGuid, userId); } public static void RefreshUserPermissionsCache(this DistributedCache dc, int userId) { - dc.Refresh(new Guid(DistributedCache.UserPermissionsCacheRefresherId), userId); + dc.Refresh(DistributedCache.UserPermissionsCacheRefresherGuid, userId); } public static void RefreshAllUserPermissionsCache(this DistributedCache dc) { - dc.RefreshAll(new Guid(DistributedCache.UserPermissionsCacheRefresherId)); + dc.RefreshAll(DistributedCache.UserPermissionsCacheRefresherGuid); } + #endregion #region Template cache - /// - /// Refreshes the cache amongst servers for a template - /// - /// - /// + public static void RefreshTemplateCache(this DistributedCache dc, int templateId) { - dc.Refresh(new Guid(DistributedCache.TemplateRefresherId), templateId); + dc.Refresh(DistributedCache.TemplateRefresherGuid, templateId); } - /// - /// Removes the cache amongst servers for a template - /// - /// - /// public static void RemoveTemplateCache(this DistributedCache dc, int templateId) { - dc.Remove(new Guid(DistributedCache.TemplateRefresherId), templateId); + dc.Remove(DistributedCache.TemplateRefresherGuid, templateId); } #endregion #region Dictionary cache - /// - /// Refreshes the cache amongst servers for a dictionary item - /// - /// - /// + public static void RefreshDictionaryCache(this DistributedCache dc, int dictionaryItemId) { - dc.Refresh(new Guid(DistributedCache.DictionaryCacheRefresherId), dictionaryItemId); + dc.Refresh(DistributedCache.DictionaryCacheRefresherGuid, dictionaryItemId); } - /// - /// Refreshes the cache amongst servers for a dictionary item - /// - /// - /// public static void RemoveDictionaryCache(this DistributedCache dc, int dictionaryItemId) { - dc.Remove(new Guid(DistributedCache.DictionaryCacheRefresherId), dictionaryItemId); + dc.Remove(DistributedCache.DictionaryCacheRefresherGuid, dictionaryItemId); } #endregion #region Data type cache - /// - /// Refreshes the cache amongst servers for a data type - /// - /// - /// + public static void RefreshDataTypeCache(this DistributedCache dc, global::umbraco.cms.businesslogic.datatype.DataTypeDefinition dataType) { - if (dataType != null) - { - dc.RefreshByJson(new Guid(DistributedCache.DataTypeCacheRefresherId), - DataTypeCacheRefresher.SerializeToJsonPayload(dataType)); - } + if (dataType == null) return; + dc.RefreshByJson(DistributedCache.DataTypeCacheRefresherGuid, DataTypeCacheRefresher.SerializeToJsonPayload(dataType)); } - /// - /// Removes the cache amongst servers for a data type - /// - /// - /// public static void RemoveDataTypeCache(this DistributedCache dc, global::umbraco.cms.businesslogic.datatype.DataTypeDefinition dataType) { - if (dataType != null) - { - dc.RefreshByJson(new Guid(DistributedCache.DataTypeCacheRefresherId), - DataTypeCacheRefresher.SerializeToJsonPayload(dataType)); - } + if (dataType == null) return; + dc.RefreshByJson(DistributedCache.DataTypeCacheRefresherGuid, DataTypeCacheRefresher.SerializeToJsonPayload(dataType)); } - /// - /// Refreshes the cache amongst servers for a data type - /// - /// - /// public static void RefreshDataTypeCache(this DistributedCache dc, IDataTypeDefinition dataType) { - if (dataType != null) - { - dc.RefreshByJson(new Guid(DistributedCache.DataTypeCacheRefresherId), - DataTypeCacheRefresher.SerializeToJsonPayload(dataType)); - } + if (dataType == null) return; + dc.RefreshByJson(DistributedCache.DataTypeCacheRefresherGuid, DataTypeCacheRefresher.SerializeToJsonPayload(dataType)); } - /// - /// Removes the cache amongst servers for a data type - /// - /// - /// public static void RemoveDataTypeCache(this DistributedCache dc, IDataTypeDefinition dataType) { - if (dataType != null) - { - dc.RefreshByJson(new Guid(DistributedCache.DataTypeCacheRefresherId), - DataTypeCacheRefresher.SerializeToJsonPayload(dataType)); - } + if (dataType == null) return; + dc.RefreshByJson(DistributedCache.DataTypeCacheRefresherGuid, DataTypeCacheRefresher.SerializeToJsonPayload(dataType)); } #endregion #region Page cache - /// - /// Refreshes the cache amongst servers for all pages - /// - /// + public static void RefreshAllPageCache(this DistributedCache dc) { - dc.RefreshAll(new Guid(DistributedCache.PageCacheRefresherId)); + dc.RefreshAll(DistributedCache.PageCacheRefresherGuid); } - /// - /// Refreshes the cache amongst servers for a page - /// - /// - /// public static void RefreshPageCache(this DistributedCache dc, int documentId) { - dc.Refresh(new Guid(DistributedCache.PageCacheRefresherId), documentId); + dc.Refresh(DistributedCache.PageCacheRefresherGuid, documentId); } - /// - /// Refreshes page cache for all instances passed in - /// - /// - /// public static void RefreshPageCache(this DistributedCache dc, params IContent[] content) { - dc.Refresh(new Guid(DistributedCache.PageCacheRefresherId), x => x.Id, content); + dc.Refresh(DistributedCache.PageCacheRefresherGuid, x => x.Id, content); } - /// - /// Removes the cache amongst servers for a page - /// - /// - /// public static void RemovePageCache(this DistributedCache dc, params IContent[] content) { - dc.Remove(new Guid(DistributedCache.PageCacheRefresherId), x => x.Id, content); + dc.Remove(DistributedCache.PageCacheRefresherGuid, x => x.Id, content); } - /// - /// Removes the cache amongst servers for a page - /// - /// - /// public static void RemovePageCache(this DistributedCache dc, int documentId) { - dc.Remove(new Guid(DistributedCache.PageCacheRefresherId), documentId); + dc.Remove(DistributedCache.PageCacheRefresherGuid, documentId); } - /// - /// invokes the unpublished page cache refresher - /// - /// - /// public static void RefreshUnpublishedPageCache(this DistributedCache dc, params IContent[] content) { - dc.Refresh(new Guid(DistributedCache.UnpublishedPageCacheRefresherId), x => x.Id, content); + dc.Refresh(DistributedCache.UnpublishedPageCacheRefresherGuid, x => x.Id, content); } - /// - /// invokes the unpublished page cache refresher - /// - /// - /// public static void RemoveUnpublishedPageCache(this DistributedCache dc, params IContent[] content) { - dc.Remove(new Guid(DistributedCache.UnpublishedPageCacheRefresherId), x => x.Id, content); + dc.Remove(DistributedCache.UnpublishedPageCacheRefresherGuid, x => x.Id, content); } - /// - /// invokes the unpublished page cache refresher to mark all ids for permanent removal - /// - /// - /// public static void RemoveUnpublishedCachePermanently(this DistributedCache dc, params int[] contentIds) { - dc.RefreshByJson(new Guid(DistributedCache.UnpublishedPageCacheRefresherId), - UnpublishedPageCacheRefresher.SerializeToJsonPayloadForPermanentDeletion(contentIds)); + dc.RefreshByJson(DistributedCache.UnpublishedPageCacheRefresherGuid, UnpublishedPageCacheRefresher.SerializeToJsonPayloadForPermanentDeletion(contentIds)); } #endregion #region Member cache - /// - /// Refreshes the cache among servers for a member - /// - /// - /// public static void RefreshMemberCache(this DistributedCache dc, params IMember[] members) { - dc.Refresh(new Guid(DistributedCache.MemberCacheRefresherId), x => x.Id, members); + dc.Refresh(DistributedCache.MemberCacheRefresherGuid, x => x.Id, members); } - /// - /// Removes the cache among servers for a member - /// - /// - /// public static void RemoveMemberCache(this DistributedCache dc, params IMember[] members) { - dc.Remove(new Guid(DistributedCache.MemberCacheRefresherId), x => x.Id, members); + dc.Remove(DistributedCache.MemberCacheRefresherGuid, x => x.Id, members); } - /// - /// Refreshes the cache among servers for a member - /// - /// - /// [Obsolete("Use the RefreshMemberCache with strongly typed IMember objects instead")] public static void RefreshMemberCache(this DistributedCache dc, int memberId) { - dc.Refresh(new Guid(DistributedCache.MemberCacheRefresherId), memberId); + dc.Refresh(DistributedCache.MemberCacheRefresherGuid, memberId); } - /// - /// Removes the cache among servers for a member - /// - /// - /// [Obsolete("Use the RemoveMemberCache with strongly typed IMember objects instead")] public static void RemoveMemberCache(this DistributedCache dc, int memberId) { - dc.Remove(new Guid(DistributedCache.MemberCacheRefresherId), memberId); + dc.Remove(DistributedCache.MemberCacheRefresherGuid, memberId); } #endregion #region Member group cache - /// - /// Refreshes the cache among servers for a member group - /// - /// - /// + public static void RefreshMemberGroupCache(this DistributedCache dc, int memberGroupId) { - dc.Refresh(new Guid(DistributedCache.MemberGroupCacheRefresherId), memberGroupId); + dc.Refresh(DistributedCache.MemberGroupCacheRefresherGuid, memberGroupId); } - /// - /// Removes the cache among servers for a member group - /// - /// - /// public static void RemoveMemberGroupCache(this DistributedCache dc, int memberGroupId) { - dc.Remove(new Guid(DistributedCache.MemberGroupCacheRefresherId), memberGroupId); + dc.Remove(DistributedCache.MemberGroupCacheRefresherGuid, memberGroupId); } #endregion #region Media Cache - /// - /// Refreshes the cache amongst servers for media items - /// - /// - /// public static void RefreshMediaCache(this DistributedCache dc, params IMedia[] media) { - dc.RefreshByJson(new Guid(DistributedCache.MediaCacheRefresherId), - MediaCacheRefresher.SerializeToJsonPayload(MediaCacheRefresher.OperationType.Saved, media)); + dc.RefreshByJson(DistributedCache.MediaCacheRefresherGuid, MediaCacheRefresher.SerializeToJsonPayload(MediaCacheRefresher.OperationType.Saved, media)); } - /// - /// Refreshes the cache amongst servers for a media item after it's been moved - /// - /// - /// public static void RefreshMediaCacheAfterMoving(this DistributedCache dc, params MoveEventInfo[] media) { - dc.RefreshByJson(new Guid(DistributedCache.MediaCacheRefresherId), - MediaCacheRefresher.SerializeToJsonPayloadForMoving( - MediaCacheRefresher.OperationType.Saved, media)); + dc.RefreshByJson(DistributedCache.MediaCacheRefresherGuid, MediaCacheRefresher.SerializeToJsonPayloadForMoving(MediaCacheRefresher.OperationType.Saved, media)); } - /// - /// Removes the cache amongst servers for a media item - /// - /// - /// - /// - /// Clearing by Id will never work for load balanced scenarios for media since we require a Path - /// to clear all of the cache but the media item will be removed before the other servers can - /// look it up. Only here for legacy purposes. - /// + // clearing by Id will never work for load balanced scenarios for media since we require a Path + // to clear all of the cache but the media item will be removed before the other servers can + // look it up. Only here for legacy purposes. [Obsolete("Ensure to clear with other RemoveMediaCache overload")] public static void RemoveMediaCache(this DistributedCache dc, int mediaId) { dc.Remove(new Guid(DistributedCache.MediaCacheRefresherId), mediaId); } - /// - /// Removes the cache among servers for media items when they are recycled - /// - /// - /// public static void RemoveMediaCacheAfterRecycling(this DistributedCache dc, params MoveEventInfo[] media) { - dc.RefreshByJson(new Guid(DistributedCache.MediaCacheRefresherId), - MediaCacheRefresher.SerializeToJsonPayloadForMoving( - MediaCacheRefresher.OperationType.Trashed, media)); + dc.RefreshByJson(DistributedCache.MediaCacheRefresherGuid, MediaCacheRefresher.SerializeToJsonPayloadForMoving(MediaCacheRefresher.OperationType.Trashed, media)); } - /// - /// Removes the cache among servers for media items when they are permanently deleted - /// - /// - /// public static void RemoveMediaCachePermanently(this DistributedCache dc, params int[] mediaIds) { - dc.RefreshByJson(new Guid(DistributedCache.MediaCacheRefresherId), - MediaCacheRefresher.SerializeToJsonPayloadForPermanentDeletion(mediaIds)); + dc.RefreshByJson(DistributedCache.MediaCacheRefresherGuid, MediaCacheRefresher.SerializeToJsonPayloadForPermanentDeletion(mediaIds)); } #endregion #region Macro Cache - /// - /// Clears the cache for all macros on the current server - /// - /// public static void ClearAllMacroCacheOnCurrentServer(this DistributedCache dc) { - //NOTE: The 'false' ensure that it will only refresh on the current server, not post to all servers - dc.RefreshAll(new Guid(DistributedCache.MacroCacheRefresherId), false); + // NOTE: The 'false' ensure that it will only refresh on the current server, not post to all servers + dc.RefreshAll(DistributedCache.MacroCacheRefresherGuid, false); } - /// - /// Refreshes the cache amongst servers for a macro item - /// - /// - /// public static void RefreshMacroCache(this DistributedCache dc, IMacro macro) { - if (macro != null) - { - dc.RefreshByJson(new Guid(DistributedCache.MacroCacheRefresherId), - MacroCacheRefresher.SerializeToJsonPayload(macro)); - } + if (macro == null) return; + dc.RefreshByJson(DistributedCache.MacroCacheRefresherGuid, MacroCacheRefresher.SerializeToJsonPayload(macro)); } - /// - /// Removes the cache amongst servers for a macro item - /// - /// - /// public static void RemoveMacroCache(this DistributedCache dc, IMacro macro) { - if (macro != null) - { - dc.RefreshByJson(new Guid(DistributedCache.MacroCacheRefresherId), - MacroCacheRefresher.SerializeToJsonPayload(macro)); - } + if (macro == null) return; + dc.RefreshByJson(DistributedCache.MacroCacheRefresherGuid, MacroCacheRefresher.SerializeToJsonPayload(macro)); } - /// - /// Refreshes the cache amongst servers for a macro item - /// - /// - /// public static void RefreshMacroCache(this DistributedCache dc, global::umbraco.cms.businesslogic.macro.Macro macro) { - if (macro != null) - { - dc.RefreshByJson(new Guid(DistributedCache.MacroCacheRefresherId), - MacroCacheRefresher.SerializeToJsonPayload(macro)); - } + if (macro == null) return; + dc.RefreshByJson(DistributedCache.MacroCacheRefresherGuid, MacroCacheRefresher.SerializeToJsonPayload(macro)); } - /// - /// Removes the cache amongst servers for a macro item - /// - /// - /// public static void RemoveMacroCache(this DistributedCache dc, global::umbraco.cms.businesslogic.macro.Macro macro) { - if (macro != null) - { - dc.RefreshByJson(new Guid(DistributedCache.MacroCacheRefresherId), - MacroCacheRefresher.SerializeToJsonPayload(macro)); - } + if (macro == null) return; + dc.RefreshByJson(DistributedCache.MacroCacheRefresherGuid, MacroCacheRefresher.SerializeToJsonPayload(macro)); } - /// - /// Removes the cache amongst servers for a macro item - /// - /// - /// public static void RemoveMacroCache(this DistributedCache dc, macro macro) { - if (macro != null && macro.Model != null) - { - dc.RefreshByJson(new Guid(DistributedCache.MacroCacheRefresherId), - MacroCacheRefresher.SerializeToJsonPayload(macro)); - } + if (macro == null || macro.Model == null) return; + dc.RefreshByJson(DistributedCache.MacroCacheRefresherGuid, MacroCacheRefresher.SerializeToJsonPayload(macro)); } + #endregion #region Document type cache - /// - /// Remove all cache for a given content type - /// - /// - /// public static void RefreshContentTypeCache(this DistributedCache dc, IContentType contentType) { - if (contentType != null) - { - dc.RefreshByJson(new Guid(DistributedCache.ContentTypeCacheRefresherId), - ContentTypeCacheRefresher.SerializeToJsonPayload(false, contentType)); - } + if (contentType == null) return; + dc.RefreshByJson(DistributedCache.ContentTypeCacheRefresherGuid, ContentTypeCacheRefresher.SerializeToJsonPayload(false, contentType)); } - /// - /// Remove all cache for a given content type - /// - /// - /// public static void RemoveContentTypeCache(this DistributedCache dc, IContentType contentType) { - if (contentType != null) - { - dc.RefreshByJson(new Guid(DistributedCache.ContentTypeCacheRefresherId), - ContentTypeCacheRefresher.SerializeToJsonPayload(true, contentType)); - } + if (contentType == null) return; + dc.RefreshByJson(DistributedCache.ContentTypeCacheRefresherGuid, ContentTypeCacheRefresher.SerializeToJsonPayload(true, contentType)); } #endregion #region Media type cache - /// - /// Remove all cache for a given media type - /// - /// - /// public static void RefreshMediaTypeCache(this DistributedCache dc, IMediaType mediaType) { - if (mediaType != null) - { - dc.RefreshByJson(new Guid(DistributedCache.ContentTypeCacheRefresherId), - ContentTypeCacheRefresher.SerializeToJsonPayload(false, mediaType)); - } + if (mediaType == null) return; + dc.RefreshByJson(DistributedCache.ContentTypeCacheRefresherGuid, ContentTypeCacheRefresher.SerializeToJsonPayload(false, mediaType)); } - /// - /// Remove all cache for a given media type - /// - /// - /// public static void RemoveMediaTypeCache(this DistributedCache dc, IMediaType mediaType) { - if (mediaType != null) - { - dc.RefreshByJson(new Guid(DistributedCache.ContentTypeCacheRefresherId), - ContentTypeCacheRefresher.SerializeToJsonPayload(true, mediaType)); - } + if (mediaType == null) return; + dc.RefreshByJson(DistributedCache.ContentTypeCacheRefresherGuid, ContentTypeCacheRefresher.SerializeToJsonPayload(true, mediaType)); } #endregion #region Media type cache - /// - /// Remove all cache for a given media type - /// - /// - /// public static void RefreshMemberTypeCache(this DistributedCache dc, IMemberType memberType) { - if (memberType != null) - { - dc.RefreshByJson(new Guid(DistributedCache.ContentTypeCacheRefresherId), - ContentTypeCacheRefresher.SerializeToJsonPayload(false, memberType)); - } + if (memberType == null) return; + dc.RefreshByJson(DistributedCache.ContentTypeCacheRefresherGuid, ContentTypeCacheRefresher.SerializeToJsonPayload(false, memberType)); } - /// - /// Remove all cache for a given media type - /// - /// - /// public static void RemoveMemberTypeCache(this DistributedCache dc, IMemberType memberType) { - if (memberType != null) - { - dc.RefreshByJson(new Guid(DistributedCache.ContentTypeCacheRefresherId), - ContentTypeCacheRefresher.SerializeToJsonPayload(true, memberType)); - } + if (memberType == null) return; + dc.RefreshByJson(DistributedCache.ContentTypeCacheRefresherGuid, ContentTypeCacheRefresher.SerializeToJsonPayload(true, memberType)); } #endregion - #region Stylesheet Cache public static void RefreshStylesheetPropertyCache(this DistributedCache dc, global::umbraco.cms.businesslogic.web.StylesheetProperty styleSheetProperty) { - if (styleSheetProperty != null) - { - dc.Refresh(new Guid(DistributedCache.StylesheetPropertyCacheRefresherId), styleSheetProperty.Id); - } + if (styleSheetProperty == null) return; + dc.Refresh(DistributedCache.StylesheetPropertyCacheRefresherGuid, styleSheetProperty.Id); } public static void RemoveStylesheetPropertyCache(this DistributedCache dc, global::umbraco.cms.businesslogic.web.StylesheetProperty styleSheetProperty) { - if (styleSheetProperty != null) - { - dc.Remove(new Guid(DistributedCache.StylesheetPropertyCacheRefresherId), styleSheetProperty.Id); - } + if (styleSheetProperty == null) return; + dc.Remove(DistributedCache.StylesheetPropertyCacheRefresherGuid, styleSheetProperty.Id); } public static void RefreshStylesheetCache(this DistributedCache dc, StyleSheet styleSheet) { - if (styleSheet != null) - { - dc.Refresh(new Guid(DistributedCache.StylesheetCacheRefresherId), styleSheet.Id); - } + if (styleSheet == null) return; + dc.Refresh(DistributedCache.StylesheetCacheRefresherGuid, styleSheet.Id); } public static void RemoveStylesheetCache(this DistributedCache dc, StyleSheet styleSheet) { - if (styleSheet != null) - { - dc.Remove(new Guid(DistributedCache.StylesheetCacheRefresherId), styleSheet.Id); - } + if (styleSheet == null) return; + dc.Remove(DistributedCache.StylesheetCacheRefresherGuid, styleSheet.Id); } public static void RefreshStylesheetCache(this DistributedCache dc, Umbraco.Core.Models.Stylesheet styleSheet) { - if (styleSheet != null) - { - dc.Refresh(new Guid(DistributedCache.StylesheetCacheRefresherId), styleSheet.Id); - } + if (styleSheet == null) return; + dc.Refresh(DistributedCache.StylesheetCacheRefresherGuid, styleSheet.Id); } public static void RemoveStylesheetCache(this DistributedCache dc, Umbraco.Core.Models.Stylesheet styleSheet) { - if (styleSheet != null) - { - dc.Remove(new Guid(DistributedCache.StylesheetCacheRefresherId), styleSheet.Id); - } + if (styleSheet == null) return; + dc.Remove(DistributedCache.StylesheetCacheRefresherGuid, styleSheet.Id); } #endregion @@ -649,18 +402,14 @@ namespace Umbraco.Web.Cache public static void RefreshDomainCache(this DistributedCache dc, IDomain domain) { - if (domain != null) - { - dc.Refresh(new Guid(DistributedCache.DomainCacheRefresherId), domain.Id); - } + if (domain == null) return; + dc.Refresh(DistributedCache.DomainCacheRefresherGuid, domain.Id); } public static void RemoveDomainCache(this DistributedCache dc, IDomain domain) { - if (domain != null) - { - dc.Remove(new Guid(DistributedCache.DomainCacheRefresherId), domain.Id); - } + if (domain == null) return; + dc.Remove(DistributedCache.DomainCacheRefresherGuid, domain.Id); } #endregion @@ -669,44 +418,38 @@ namespace Umbraco.Web.Cache public static void RefreshLanguageCache(this DistributedCache dc, ILanguage language) { - if (language != null) - { - dc.Refresh(new Guid(DistributedCache.LanguageCacheRefresherId), language.Id); - } + if (language == null) return; + dc.Refresh(DistributedCache.LanguageCacheRefresherGuid, language.Id); } public static void RemoveLanguageCache(this DistributedCache dc, ILanguage language) { - if (language != null) - { - dc.Remove(new Guid(DistributedCache.LanguageCacheRefresherId), language.Id); - } + if (language == null) return; + dc.Remove(DistributedCache.LanguageCacheRefresherGuid, language.Id); } public static void RefreshLanguageCache(this DistributedCache dc, global::umbraco.cms.businesslogic.language.Language language) { - if (language != null) - { - dc.Refresh(new Guid(DistributedCache.LanguageCacheRefresherId), language.id); - } + if (language == null) return; + dc.Refresh(DistributedCache.LanguageCacheRefresherGuid, language.id); } public static void RemoveLanguageCache(this DistributedCache dc, global::umbraco.cms.businesslogic.language.Language language) { - if (language != null) - { - dc.Remove(new Guid(DistributedCache.LanguageCacheRefresherId), language.id); - } + if (language == null) return; + dc.Remove(DistributedCache.LanguageCacheRefresherGuid, language.id); } #endregion + #region Xslt Cache + public static void ClearXsltCacheOnCurrentServer(this DistributedCache dc) { - if (UmbracoConfig.For.UmbracoSettings().Content.UmbracoLibraryCacheDuration > 0) - { - ApplicationContext.Current.ApplicationCache.ClearCacheObjectTypes("MS.Internal.Xml.XPath.XPathSelectionIterator"); - } + if (UmbracoConfig.For.UmbracoSettings().Content.UmbracoLibraryCacheDuration <= 0) return; + ApplicationContext.Current.ApplicationCache.ClearCacheObjectTypes("MS.Internal.Xml.XPath.XPathSelectionIterator"); } + + #endregion } } \ No newline at end of file diff --git a/src/Umbraco.Web/Routing/EnsureRoutableOutcome.cs b/src/Umbraco.Web/Routing/EnsureRoutableOutcome.cs index 9060b9f773..fd09f5ff8c 100644 --- a/src/Umbraco.Web/Routing/EnsureRoutableOutcome.cs +++ b/src/Umbraco.Web/Routing/EnsureRoutableOutcome.cs @@ -1,14 +1,44 @@ namespace Umbraco.Web.Routing { /// - /// Reasons a request was not routable on the front-end + /// Represents the outcome of trying to route an incoming request. /// internal enum EnsureRoutableOutcome { + /// + /// Request routes to a document. + /// + /// + /// Umbraco was ready and configured, and has content. + /// The request looks like it can be a route to a document. This does not + /// mean that there *is* a matching document, ie the request might end up returning + /// 404. + /// IsRoutable = 0, + + /// + /// Request does not route to a document. + /// + /// + /// Umbraco was ready and configured, and has content. + /// The request does not look like it can be a route to a document. Can be + /// anything else eg back-office, surface controller... + /// NotDocumentRequest = 10, + + /// + /// Umbraco was not ready. + /// NotReady = 11, + + /// + /// Umbraco was not configured. + /// NotConfigured = 12, + + /// + /// There was no content at all. + /// NoContent = 13 } } \ No newline at end of file diff --git a/src/Umbraco.Web/Strategies/ServerRegistrationEventHandler.cs b/src/Umbraco.Web/Strategies/ServerRegistrationEventHandler.cs index ab478c6c2b..6d8faa782f 100644 --- a/src/Umbraco.Web/Strategies/ServerRegistrationEventHandler.cs +++ b/src/Umbraco.Web/Strategies/ServerRegistrationEventHandler.cs @@ -1,149 +1,95 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading; using System.Web; +using Newtonsoft.Json; using Umbraco.Core; -using Umbraco.Core.Configuration; using Umbraco.Core.Logging; -using Umbraco.Core.Services; using Umbraco.Core.Sync; using Umbraco.Web.Routing; namespace Umbraco.Web.Strategies { /// - /// This will ensure that the server is automatically registered in the database as an active node - /// on application startup and whenever a back office request occurs. + /// Ensures that servers are automatically registered in the database, when using the database server registrar. /// /// - /// We do this on app startup to ensure that the server is in the database but we also do it for the first 'x' times - /// a back office request is made so that we can tell if they are using https protocol which would update to that address - /// in the database. The first front-end request probably wouldn't be an https request. - /// - /// For back office requests (so that we don't constantly make db calls), we'll only update the database when we detect at least - /// a timespan of 1 minute between requests. + /// At the moment servers are automatically registered upon first request and then on every + /// request but not more than once per (configurable) period. This really is "for information & debug" purposes so + /// we can look at the table and see what servers are registered - but the info is not used anywhere. + /// Should we actually want to use this, we would need a better and more deterministic way of figuring + /// out the "server address" ie the address to which server-to-server requests should be sent - because it + /// probably is not the "current request address" - especially in multi-domains configurations. /// public sealed class ServerRegistrationEventHandler : ApplicationEventHandler { - private static bool _initUpdated = false; private static DateTime _lastUpdated = DateTime.MinValue; - private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim(); - - //protected override void ApplicationStarting(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) - //{ - // ServerRegistrarResolver.Current.SetServerRegistrar( - // new DatabaseServerRegistrar( - // new Lazy(() => applicationContext.Services.ServerRegistrationService))); - //} - - /// - /// Update the database with this entry and bind to request events - /// - /// - /// + // bind to events protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) { - //no need to bind to the event if we are not actually using the database server registrar + // only for the DatabaseServerRegistrar if (ServerRegistrarResolver.Current.Registrar is DatabaseServerRegistrar) - { - //bind to event - UmbracoModule.RouteAttempt += UmbracoModuleRouteAttempt; - } + UmbracoModule.RouteAttempt += UmbracoModuleRouteAttempt; } - - static void UmbracoModuleRouteAttempt(object sender, RoutableAttemptEventArgs e) + // handles route attempts. + private static void UmbracoModuleRouteAttempt(object sender, RoutableAttemptEventArgs e) { if (e.HttpContext.Request == null || e.HttpContext.Request.Url == null) return; - if (e.Outcome == EnsureRoutableOutcome.IsRoutable) + switch (e.Outcome) { - using (var lck = new UpgradeableReadLock(Locker)) - { - //we only want to do the initial update once - if (!_initUpdated) - { - lck.UpgradeToWriteLock(); - _initUpdated = true; - UpdateServerEntry(e.HttpContext, e.UmbracoContext.Application); - return; - } - } - } - - //if it is not a document request, we'll check if it is a back end request - if (e.Outcome == EnsureRoutableOutcome.NotDocumentRequest) - { - //check if this is in the umbraco back office - if (e.HttpContext.Request.Url.IsBackOfficeRequest(HttpRuntime.AppDomainAppVirtualPath)) - { - //yup it's a back office request! - using (var lck = new UpgradeableReadLock(Locker)) - { - //we don't want to update if it's not been at least a minute since last time - var isItAMinute = DateTime.Now.Subtract(_lastUpdated).TotalSeconds >= 60; - if (isItAMinute) - { - lck.UpgradeToWriteLock(); - _initUpdated = true; - _lastUpdated = DateTime.Now; - UpdateServerEntry(e.HttpContext, e.UmbracoContext.Application); - } - } - } + case EnsureRoutableOutcome.IsRoutable: + // front-end request + RegisterServer(e); + break; + case EnsureRoutableOutcome.NotDocumentRequest: + // anything else (back-end request, service...) + //so it's not a document request, we'll check if it's a back office request + if (e.HttpContext.Request.Url.IsBackOfficeRequest(HttpRuntime.AppDomainAppVirtualPath)) + RegisterServer(e); + break; + /* + case EnsureRoutableOutcome.NotReady: + case EnsureRoutableOutcome.NotConfigured: + case EnsureRoutableOutcome.NoContent: + default: + // otherwise, do nothing + break; + */ } } - - private static void UpdateServerEntry(HttpContextBase httpContext, ApplicationContext applicationContext) + // register current server (throttled). + private static 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; + + _lastUpdated = DateTime.Now; + + var url = e.HttpContext.Request.Url; + var svc = e.UmbracoContext.Application.Services.ServerRegistrationService; + try { - var address = httpContext.Request.Url.GetLeftPart(UriPartial.Authority); - applicationContext.Services.ServerRegistrationService.EnsureActive(address); + 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); } - catch (Exception e) + catch (Exception ex) { - LogHelper.Error("Failed to update server record in database.", e); + LogHelper.Error("Failed to update server record in database.", ex); } } - - //private static IEnumerable> GetBindings(HttpContextBase context) - //{ - // // Get the Site name - // string siteName = System.Web.Hosting.HostingEnvironment.SiteName; - - // // Get the sites section from the AppPool.config - // Microsoft.Web.Administration.ConfigurationSection sitesSection = - // Microsoft.Web.Administration.WebConfigurationManager.GetSection(null, null, "system.applicationHost/sites"); - - // foreach (Microsoft.Web.Administration.ConfigurationElement site in sitesSection.GetCollection()) - // { - // // Find the right Site - // if (String.Equals((string)site["name"], siteName, StringComparison.OrdinalIgnoreCase)) - // { - - // // For each binding see if they are http based and return the port and protocol - // foreach (Microsoft.Web.Administration.ConfigurationElement binding in site.GetCollection("bindings")) - // { - // string protocol = (string)binding["protocol"]; - // string bindingInfo = (string)binding["bindingInformation"]; - - // if (protocol.StartsWith("http", StringComparison.OrdinalIgnoreCase)) - // { - // string[] parts = bindingInfo.Split(':'); - // if (parts.Length == 3) - // { - // string port = parts[1]; - // yield return new KeyValuePair(protocol, port); - // } - // } - // } - // } - // } - //} } } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 1ce693b27a..4bb3b41651 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -132,6 +132,10 @@ ..\packages\Lucene.Net.2.9.4.1\lib\net40\Lucene.Net.dll + + ..\packages\Microsoft.Web.Administration.7.0.0.0\lib\net20\Microsoft.Web.Administration.dll + True + True ..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll @@ -277,6 +281,7 @@ + @@ -287,7 +292,8 @@ - + + @@ -1884,6 +1890,7 @@ True Reference.map + diff --git a/src/Umbraco.Web/WebBootManager.cs b/src/Umbraco.Web/WebBootManager.cs index 4a7b78c683..787db1f12c 100644 --- a/src/Umbraco.Web/WebBootManager.cs +++ b/src/Umbraco.Web/WebBootManager.cs @@ -11,6 +11,7 @@ using System.Web.Mvc; using System.Web.Routing; using ClientDependency.Core.Config; using Examine; +using umbraco; using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.Dictionary; @@ -37,6 +38,7 @@ using Umbraco.Web.Scheduling; using Umbraco.Web.UI.JavaScript; using Umbraco.Web.WebApi; using umbraco.BusinessLogic; +using GlobalSettings = Umbraco.Core.Configuration.GlobalSettings; using ProfilingViewEngine = Umbraco.Core.Profiling.ProfilingViewEngine; @@ -49,7 +51,7 @@ namespace Umbraco.Web { private readonly bool _isForTesting; //NOTE: see the Initialize method for what this is used for - private List _indexesToRebuild = new List(); + private readonly List _indexesToRebuild = new List(); public WebBootManager(UmbracoApplicationBase umbracoApplication) : this(umbracoApplication, false) @@ -320,13 +322,18 @@ namespace Umbraco.Web //set the default RenderMvcController DefaultRenderMvcControllerResolver.Current = new DefaultRenderMvcControllerResolver(typeof(RenderMvcController)); - //Override the ServerMessengerResolver to set a username/password for the distributed calls - ServerMessengerResolver.Current.SetServerMessenger(new BatchedServerMessenger(() => + ServerMessengerResolver.Current.SetServerMessenger(new BatchedWebServiceServerMessenger(() => { //we should not proceed to change this if the app/database is not configured since there will // be no user, plus we don't need to have server messages sent if this is the case. if (ApplicationContext.IsConfigured && ApplicationContext.DatabaseContext.IsDatabaseConfigured) { + //disable if they are not enabled + if (UmbracoConfig.For.UmbracoSettings().DistributedCall.Enabled == false) + { + return null; + } + try { var user = User.GetUser(UmbracoConfig.For.UmbracoSettings().DistributedCall.UserId); diff --git a/src/Umbraco.Web/WebServerUtility.cs b/src/Umbraco.Web/WebServerUtility.cs new file mode 100644 index 0000000000..7ea9e8ea25 --- /dev/null +++ b/src/Umbraco.Web/WebServerUtility.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web.Hosting; +using Umbraco.Core; +using Microsoft.Web.Administration; + +namespace Umbraco.Web +{ + internal class WebServerUtility + { + // NOTE + // + // there's some confusion with Microsoft.Web.Administration versions + // 7.0.0.0 is installed by NuGet and will read IIS settings + // 7.9.0.0 comes with IIS Express and will read IIS Express + // we want to use 7.0.0.0 when building + // and then... there are further versions that are N/A on NuGet + // + // Umbraco uses 7.0.0.0 from NuGet + // IMPORTANT: and then, the reference's SpecificVersion property MUST be set to true + // otherwise we might build with 7.9.0.0 and end up in troubles (reading IIS Express + // instead of IIS even when running IIS) - IIS Express has a binding redirect from + // 7.0.0.0 to 7.9.0.0 so it's fine. + // + // read: + // http://stackoverflow.com/questions/11208270/microsoft-web-administration-servermanager-looking-in-wrong-directory-for-iisexp + // http://stackoverflow.com/questions/8467908/how-to-use-servermanager-to-read-iis-sites-not-iis-express-from-class-library + // http://stackoverflow.com/questions/25812169/microsoft-web-administration-servermanager-is-connecting-to-the-iis-express-inst + + public static IEnumerable GetBindings() + { + // FIXME + // which of these methods shall we use? + // what about permissions, trust, etc? + + //return GetBindings2(); + throw new NotImplementedException(); + } + + private static IEnumerable GetBindings1() + { + // get the site name + var siteName = HostingEnvironment.SiteName; + + // get the site from the sites section from the AppPool.config + var sitesSection = WebConfigurationManager.GetSection(null, null, "system.applicationHost/sites"); + var site = sitesSection.GetCollection().FirstOrDefault(x => ((string) x["name"]).InvariantEquals(siteName)); + if (site == null) + return Enumerable.Empty(); + + return site.GetCollection("bindings") + .Where(x => ((string) x["protocol"]).StartsWith("http", StringComparison.OrdinalIgnoreCase)) + .Select(x => + { + var bindingInfo = (string) x["bindingInformation"]; + var parts = bindingInfo.Split(':'); // can count be != 3 ?? + return new Uri(x["protocol"] + "://" + parts[2] + ":" + parts[1] + "/"); + }); + } + + private static IEnumerable GetBindings2() + { + // get the site name + var siteName = HostingEnvironment.SiteName; + + // get the site from the server manager + var mgr = new ServerManager(); + var site = mgr.Sites.FirstOrDefault(x => x.Name.InvariantEquals(siteName)); + if (site == null) + return Enumerable.Empty(); + + // get the bindings + return site.Bindings + .Where(x => x.Protocol.StartsWith("http", StringComparison.OrdinalIgnoreCase)) + .Select(x => new Uri(x.Protocol + "://" + x.Host + ":" + x.EndPoint.Port + "/")); + } + } +} diff --git a/src/Umbraco.Web/packages.config b/src/Umbraco.Web/packages.config index b08c9193ee..f624d5b6d5 100644 --- a/src/Umbraco.Web/packages.config +++ b/src/Umbraco.Web/packages.config @@ -17,6 +17,7 @@ + diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/CacheRefresher.asmx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/CacheRefresher.asmx.cs index 5bcd3cab79..4186b2446a 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/CacheRefresher.asmx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/CacheRefresher.asmx.cs @@ -1,99 +1,104 @@ using System; -using System.Collections; -using System.Collections.Generic; -using System.ComponentModel; -using System.Data; -using System.Diagnostics; using System.Linq; using System.Web; -using System.Web.Script.Serialization; using System.Web.Services; using System.Xml; +using Newtonsoft.Json; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Logging; using Umbraco.Core.Sync; +using umbraco.interfaces; namespace umbraco.presentation.webservices { - /// - /// Summary description for CacheRefresher. + /// CacheRefresher web service. /// [WebService(Namespace="http://umbraco.org/webservices/")] public class CacheRefresher : WebService - { + { + #region Helpers - /// - /// This checks the passed in hash and verifies if it does not match the hash of the combination of appDomainAppId and machineName - /// passed in. If the hashes don't match, then cache refreshing continues. - /// - /// - /// - /// - /// - internal bool ContinueRefreshingForRequest(string hash, string appDomainAppId, string machineName) - { - //check if this is the same app id as the one passed in, if it is, then we will ignore - // the request - we will have to assume that the cache refreshing has already been applied to the server - // that executed the request. - if (hash.IsNullOrWhiteSpace() == false && SystemUtilities.GetCurrentTrustLevel() == AspNetHostingPermissionLevel.Unrestricted) - { - var hasher = new HashCodeCombiner(); - hasher.AddCaseInsensitiveString(machineName); - hasher.AddCaseInsensitiveString(appDomainAppId); - var hashedAppId = hasher.GetCombinedHashCode(); + // is the server originating from this server - ie are we self-messaging? + // in which case we should ignore the message because it's been processed locally already + internal static bool SelfMessage(string hash) + { + if (hash != WebServiceServerMessenger.GetCurrentServerHash()) return false; - //we can only check this in full trust. if it's in medium trust we'll just end up with - // the server refreshing it's cache twice. - if (hashedAppId == hash) - { - LogHelper.Debug( - "The passed in hashed appId equals the current server's hashed appId, cache refreshing will be ignored for this request as it will have already executed for this server (server: {0} , appId: {1} , hash: {2})", - () => machineName, - () => appDomainAppId, - () => hashedAppId); + LogHelper.Debug( + "Ignoring self-message. (server: {0}, appId: {1}, hash: {2})", + () => NetworkHelper.MachineName, + () => HttpRuntime.AppDomainAppId, + () => hash); - return false; - } - } + return true; + } - return true; - } + private static ICacheRefresher GetRefresher(Guid id) + { + var refresher = CacheRefreshersResolver.Current.GetById(id); + if (refresher == null) + throw new InvalidOperationException("Cache refresher with ID \"" + id + "\" does not exist."); + return refresher; + } + + private static IJsonCacheRefresher GetJsonRefresher(Guid id) + { + return GetJsonRefresher(GetRefresher(id)); + } + + private static IJsonCacheRefresher GetJsonRefresher(ICacheRefresher refresher) + { + var jsonRefresher = refresher as IJsonCacheRefresher; + if (jsonRefresher == null) + throw new InvalidOperationException("Cache refresher with ID \"" + refresher.UniqueIdentifier + "\" does not implement " + typeof(IJsonCacheRefresher) + "."); + return jsonRefresher; + } + + private static bool NotAutorized(string login, string rawPassword) + { + var user = ApplicationContext.Current.Services.UserService.GetByUsername(login); + return user == null || user.RawPasswordValue != rawPassword; + } + + #endregion [WebMethod] public void BulkRefresh(RefreshInstruction[] instructions, string appId, string login, string password) { - if (BusinessLogic.User.validateCredentials(login, password) == false) - { - return; - } + if (NotAutorized(login, password)) return; + if (SelfMessage(appId)) return; // do not process self-messages - if (ContinueRefreshingForRequest(appId, HttpRuntime.AppDomainAppId, NetworkHelper.MachineName) == false) return; - - //only execute distinct instructions - no sense in running the same one. + // only execute distinct instructions - no sense in running the same one more than once foreach (var instruction in instructions.Distinct()) { + var refresher = GetRefresher(instruction.RefresherId); switch (instruction.RefreshType) { - case RefreshInstruction.RefreshMethodType.RefreshAll: - RefreshAll(instruction.RefresherId); + case RefreshMethodType.RefreshAll: + refresher.RefreshAll(); break; - case RefreshInstruction.RefreshMethodType.RefreshByGuid: - RefreshByGuid(instruction.RefresherId, instruction.GuidId); + case RefreshMethodType.RefreshByGuid: + refresher.Refresh(instruction.GuidId); break; - case RefreshInstruction.RefreshMethodType.RefreshById: - RefreshById(instruction.RefresherId, instruction.IntId); + case RefreshMethodType.RefreshById: + refresher.Refresh(instruction.IntId); break; - case RefreshInstruction.RefreshMethodType.RefreshByIds: - RefreshByIds(instruction.RefresherId, instruction.JsonIds); + case RefreshMethodType.RefreshByIds: // not directly supported by ICacheRefresher + foreach (var id in JsonConvert.DeserializeObject(instruction.JsonIds)) + refresher.Refresh(id); break; - case RefreshInstruction.RefreshMethodType.RefreshByJson: - RefreshByJson(instruction.RefresherId, instruction.JsonPayload); + case RefreshMethodType.RefreshByJson: + GetJsonRefresher(refresher).Refresh(instruction.JsonPayload); break; - case RefreshInstruction.RefreshMethodType.RemoveById: - RemoveById(instruction.RefresherId, instruction.IntId); + case RefreshMethodType.RemoveById: + refresher.Remove(instruction.IntId); break; + //case RefreshMethodType.RemoveByIds: // not directly supported by ICacheRefresher + // foreach (var id in JsonConvert.DeserializeObject(instruction.JsonIds)) + // refresher.Remove(id); + // break; } } } @@ -101,139 +106,61 @@ namespace umbraco.presentation.webservices [WebMethod] public void RefreshAll(Guid uniqueIdentifier, string Login, string Password) { - if (BusinessLogic.User.validateCredentials(Login, Password)) - { - RefreshAll(uniqueIdentifier); - } + if (NotAutorized(Login, Password)) return; + GetRefresher(uniqueIdentifier).RefreshAll(); } - private void RefreshAll(Guid uniqueIdentifier) - { - var cr = CacheRefreshersResolver.Current.GetById(uniqueIdentifier); - cr.RefreshAll(); - } - [WebMethod] public void RefreshByGuid(Guid uniqueIdentifier, Guid Id, string Login, string Password) { - if (BusinessLogic.User.validateCredentials(Login, Password)) - { - RefreshByGuid(uniqueIdentifier, Id); - } + if (NotAutorized(Login, Password)) return; + GetRefresher(uniqueIdentifier).Refresh(Id); } - private void RefreshByGuid(Guid uniqueIdentifier, Guid Id) - { - var cr = CacheRefreshersResolver.Current.GetById(uniqueIdentifier); - cr.Refresh(Id); - } - [WebMethod] public void RefreshById(Guid uniqueIdentifier, int Id, string Login, string Password) { - if (BusinessLogic.User.validateCredentials(Login, Password)) - { - RefreshById(uniqueIdentifier, Id); - } + if (NotAutorized(Login, Password)) return; + GetRefresher(uniqueIdentifier).Refresh(Id); } - private void RefreshById(Guid uniqueIdentifier, int Id) - { - var cr = CacheRefreshersResolver.Current.GetById(uniqueIdentifier); - cr.Refresh(Id); - } - - /// - /// Refreshes objects for all Ids matched in the json string - /// - /// - /// A JSON Serialized string of ids to match - /// - /// [WebMethod] public void RefreshByIds(Guid uniqueIdentifier, string jsonIds, string Login, string Password) { - if (BusinessLogic.User.validateCredentials(Login, Password)) - { - RefreshByIds(uniqueIdentifier, jsonIds); - } + if (NotAutorized(Login, Password)) return; + var refresher = GetRefresher(uniqueIdentifier); + foreach (var id in JsonConvert.DeserializeObject(jsonIds)) + refresher.Refresh(id); } - private void RefreshByIds(Guid uniqueIdentifier, string jsonIds) - { - var serializer = new JavaScriptSerializer(); - var ids = serializer.Deserialize(jsonIds); - - var cr = CacheRefreshersResolver.Current.GetById(uniqueIdentifier); - foreach (var i in ids) - { - cr.Refresh(i); - } - } - - /// - /// Refreshes objects using the passed in Json payload, it will be up to the cache refreshers to deserialize - /// - /// - /// A custom JSON payload used by the cache refresher - /// - /// - /// - /// NOTE: the cache refresher defined by the ID MUST be of type IJsonCacheRefresher or an exception will be thrown - /// [WebMethod] public void RefreshByJson(Guid uniqueIdentifier, string jsonPayload, string Login, string Password) - { - if (BusinessLogic.User.validateCredentials(Login, Password)) - { - RefreshByJson(uniqueIdentifier, jsonPayload); - } + { + if (NotAutorized(Login, Password)) return; + GetJsonRefresher(uniqueIdentifier).Refresh(jsonPayload); } - private void RefreshByJson(Guid uniqueIdentifier, string jsonPayload) - { - var cr = CacheRefreshersResolver.Current.GetById(uniqueIdentifier) as IJsonCacheRefresher; - if (cr == null) - { - throw new InvalidOperationException("The cache refresher: " + uniqueIdentifier + " is not of type " + typeof(IJsonCacheRefresher)); - } - cr.Refresh(jsonPayload); - } - [WebMethod] public void RemoveById(Guid uniqueIdentifier, int Id, string Login, string Password) { - if (BusinessLogic.User.validateCredentials(Login, Password)) - { - RemoveById(uniqueIdentifier, Id); - } + if (NotAutorized(Login, Password)) return; + GetRefresher(uniqueIdentifier).Remove(Id); } - private void RemoveById(Guid uniqueIdentifier, int Id) - { - var cr = CacheRefreshersResolver.Current.GetById(uniqueIdentifier); - cr.Remove(Id); - } - [WebMethod] - public XmlDocument GetRefreshers(string Login, string Password) - { - if (BusinessLogic.User.validateCredentials(Login, Password)) - { - var xd = new XmlDocument(); - xd.LoadXml(""); - foreach (var cr in CacheRefreshersResolver.Current.CacheRefreshers) - { - var n = xmlHelper.addTextNode(xd, "cacheRefresher", cr.Name); - n.Attributes.Append(xmlHelper.addAttribute(xd, "uniqueIdentifier", cr.UniqueIdentifier.ToString())); - xd.DocumentElement.AppendChild(n); - } - return xd; - - - } - return null; - } + public XmlDocument GetRefreshers(string Login, string Password) + { + if (NotAutorized(Login, Password)) return null; + var xd = new XmlDocument(); + xd.LoadXml(""); + foreach (var cr in CacheRefreshersResolver.Current.CacheRefreshers) + { + var n = xmlHelper.addTextNode(xd, "cacheRefresher", cr.Name); + n.Attributes.Append(xmlHelper.addAttribute(xd, "uniqueIdentifier", cr.UniqueIdentifier.ToString())); + xd.DocumentElement.AppendChild(n); + } + return xd; + } } } diff --git a/src/umbraco.interfaces/ICacheRefresher.cs b/src/umbraco.interfaces/ICacheRefresher.cs index 21f454ca66..d239e81fe6 100644 --- a/src/umbraco.interfaces/ICacheRefresher.cs +++ b/src/umbraco.interfaces/ICacheRefresher.cs @@ -15,6 +15,8 @@ namespace umbraco.interfaces void Refresh(int Id); void Remove(int Id); void Refresh(Guid Id); + + //void Notify(object payload); } }