Merge branch 'dev-v7-distcache' into 7.3.0

This commit is contained in:
Shannon
2015-04-01 14:00:13 +11:00
49 changed files with 2490 additions and 1837 deletions
+1 -1
View File
@@ -327,7 +327,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,
@@ -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
{
/// <summary>
/// Allows for outputting a normalized appdomainappid token in a log format
/// </summary>
public sealed class AppDomainTokenConverter : log4net.Util.PatternConverter
{
protected override void Convert(TextWriter writer, object state)
{
writer.Write(HttpRuntime.AppDomainAppId.ReplaceNonAlphanumericChars(string.Empty));
}
}
}
@@ -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; }
}
}
@@ -15,22 +15,19 @@ namespace Umbraco.Core.Models.Rdbms
[Column("address")]
[Length(500)]
public string Address { get; set; }
public string ServerAddress { get; set; }
/// <summary>
/// A unique column in the database, a computer name must always be unique!
/// </summary>
[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)]
+59 -33
View File
@@ -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
/// <summary>
/// Represents a registered server in a multiple-servers environment.
/// </summary>
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<ServerRegistration, string>(x => x.ServerAddress);
private static readonly PropertyInfo ComputerNameSelector = ExpressionHelper.GetPropertyInfo<ServerRegistration, string>(x => x.ComputerName);
private static readonly PropertyInfo ServerIdentitySelector = ExpressionHelper.GetPropertyInfo<ServerRegistration, string>(x => x.ServerIdentity);
private static readonly PropertyInfo IsActiveSelector = ExpressionHelper.GetPropertyInfo<ServerRegistration, bool>(x => x.IsActive);
/// <summary>
/// Initialiazes a new instance of the <see cref="ServerRegistration"/> class.
/// </summary>
public ServerRegistration()
{
}
{ }
/// <summary>
/// Creates an item with pre-filled properties
/// Initialiazes a new instance of the <see cref="ServerRegistration"/> class.
/// </summary>
/// <param name="id"></param>
/// <param name="serverAddress"></param>
/// <param name="computerName"></param>
/// <param name="createDate"></param>
/// <param name="updateDate"></param>
/// <param name="isActive"></param>
public ServerRegistration(int id, string serverAddress, string computerName, DateTime createDate, DateTime updateDate, bool isActive)
/// <param name="id">The unique id of the server registration.</param>
/// <param name="serverAddress">The server url.</param>
/// <param name="serverIdentity">The unique server identity.</param>
/// <param name="registered">The date and time the registration was created.</param>
/// <param name="accessed">The date and time the registration was last accessed.</param>
/// <param name="isActive">A value indicating whether the registration is active.</param>
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;
}
/// <summary>
/// Creates a new instance for persisting a new item
/// Initialiazes a new instance of the <see cref="ServerRegistration"/> class.
/// </summary>
/// <param name="serverAddress"></param>
/// <param name="computerName"></param>
/// <param name="createDate"></param>
public ServerRegistration(string serverAddress, string computerName, DateTime createDate)
/// <param name="serverAddress">The server url.</param>
/// <param name="serverIdentity">The unique server identity.</param>
/// <param name="registered">The date and time the registration was created.</param>
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;
}
/// <summary>
/// Gets or sets the server url.
/// </summary>
public string ServerAddress
{
get { return _serverAddress; }
@@ -70,19 +76,25 @@ namespace Umbraco.Core.Models
}
}
public string ComputerName
/// <summary>
/// Gets or sets the server unique identity.
/// </summary>
public string ServerIdentity
{
get { return _computerName; }
get { return _serverIdentity; }
set
{
SetPropertyValueAndDetectChanges(o =>
{
_computerName = value;
return _computerName;
}, _computerName, ComputerNameSelector);
_serverIdentity = value;
return _serverIdentity;
}, _serverIdentity, ServerIdentitySelector);
}
}
/// <summary>
/// Gets or sets a value indicating whether the server is active.
/// </summary>
public bool IsActive
{
get { return _isActive; }
@@ -96,9 +108,23 @@ namespace Umbraco.Core.Models
}
}
/// <summary>
/// Gets the date and time the registration was created.
/// </summary>
public DateTime Registered { get { return CreateDate; } set { CreateDate = value; }}
/// <summary>
/// Gets the date and time the registration was last accessed.
/// </summary>
public DateTime Accessed { get { return UpdateDate; } set { UpdateDate = value; }}
/// <summary>
/// Converts the value of this instance to its equivalent string representation.
/// </summary>
/// <returns></returns>
public override string ToString()
{
return "(" + ServerAddress + ", " + ComputerName + ", IsActive = " + IsActive + ")";
return string.Format("{{\"{0}\", \"{1}\", {2}active}}", ServerAddress, ServerIdentity, IsActive ? "" : "!");
}
}
}
@@ -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;
}
@@ -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<string, DtoMapModel> PropertyInfoCacheInstance = new ConcurrentDictionary<string, DtoMapModel>();
@@ -29,10 +29,10 @@ namespace Umbraco.Core.Persistence.Mappers
{
CacheMap<ServerRegistration, ServerRegistrationDto>(src => src.Id, dto => dto.Id);
CacheMap<ServerRegistration, ServerRegistrationDto>(src => src.IsActive, dto => dto.IsActive);
CacheMap<ServerRegistration, ServerRegistrationDto>(src => src.ServerAddress, dto => dto.Address);
CacheMap<ServerRegistration, ServerRegistrationDto>(src => src.ServerAddress, dto => dto.ServerAddress);
CacheMap<ServerRegistration, ServerRegistrationDto>(src => src.CreateDate, dto => dto.DateRegistered);
CacheMap<ServerRegistration, ServerRegistrationDto>(src => src.UpdateDate, dto => dto.LastNotified);
CacheMap<ServerRegistration, ServerRegistrationDto>(src => src.ComputerName, dto => dto.ComputerName);
CacheMap<ServerRegistration, ServerRegistrationDto>(src => src.UpdateDate, dto => dto.DateAccessed);
CacheMap<ServerRegistration, ServerRegistrationDto>(src => src.ServerIdentity, dto => dto.ServerIdentity);
}
#endregion
@@ -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
@@ -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");
}
}
}
@@ -125,5 +125,12 @@ namespace Umbraco.Core.Persistence.Repositories
entity.ResetDirtyProperties();
}
public void DeactiveStaleServers(TimeSpan staleTimeout)
{
var timeoutDate = DateTime.UtcNow.Subtract(staleTimeout);
Database.Update<ServerRegistrationDto>("SET isActive=0 WHERE lastNotifiedDate < @timeoutDate", new {timeoutDate = timeoutDate});
}
}
}
@@ -11,63 +11,66 @@ namespace Umbraco.Core.Services
{
/// <summary>
/// Service to manage server registrations in the database
/// Manages server registrations in the database.
/// </summary>
internal class ServerRegistrationService : RepositoryService
public sealed class ServerRegistrationService : RepositoryService
{
public ServerRegistrationService(IDatabaseUnitOfWorkProvider provider, RepositoryFactory repositoryFactory, ILogger logger)
: base(provider, repositoryFactory, logger)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ServerRegistrationService"/> class.
/// </summary>
/// <param name="uowProvider">A UnitOfWork provider.</param>
/// <param name="repositoryFactory">A repository factory.</param>
/// <param name="logger">A logger.</param>
public ServerRegistrationService(IDatabaseUnitOfWorkProvider uowProvider, RepositoryFactory repositoryFactory, ILogger logger)
: base(uowProvider, repositoryFactory, logger)
{ }
/// <summary>
/// Called to 'call home' to ensure the current server has an active record
/// Touches a server to mark it as active; deactivate stale servers.
/// </summary>
/// <param name="address"></param>
public void EnsureActive(string address)
/// <param name="serverAddress">The server url.</param>
/// <param name="serverIdentity">The server unique identity.</param>
/// <param name="staleTimeout">The time after which a server is considered stale.</param>
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<ServerRegistration>.Builder.Where(x => x.ComputerName.ToUpper() == computerName.ToUpper());
var found = repo.GetByQuery(query).ToArray();
ServerRegistration server;
if (found.Any())
var query = Query<ServerRegistration>.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);
}
}
/// <summary>
/// Deactivates a server by name
/// Deactivates a server.
/// </summary>
/// <param name="computerName"></param>
public void DeactiveServer(string computerName)
/// <param name="serverIdentity">The server unique identity.</param>
public void DeactiveServer(string serverIdentity)
{
var uow = UowProvider.GetUnitOfWork();
using (var repo = RepositoryFactory.CreateServerRegistrationRepository(uow))
{
var query = Query<ServerRegistration>.Builder.Where(x => x.ComputerName.ToUpper() == computerName.ToUpper());
var found = repo.GetByQuery(query).ToArray();
if (found.Any())
var query = Query<ServerRegistration>.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
}
/// <summary>
/// Return all active servers
/// Deactivates stale servers.
/// </summary>
/// <param name="staleTimeout">The time after which a server is considered stale.</param>
public void DeactiveStaleServers(TimeSpan staleTimeout)
{
var uow = UowProvider.GetUnitOfWork();
using (var repo = RepositoryFactory.CreateServerRegistrationRepository(uow))
{
repo.DeactiveStaleServers(staleTimeout);
}
}
/// <summary>
/// Return all active servers.
/// </summary>
/// <returns></returns>
public IEnumerable<ServerRegistration> GetActiveServers()
+1 -1
View File
@@ -274,7 +274,7 @@ namespace Umbraco.Core.Services
/// <summary>
/// Gets the <see cref="ServerRegistrationService"/>
/// </summary>
internal ServerRegistrationService ServerRegistrationService
public ServerRegistrationService ServerRegistrationService
{
get { return _serverRegistrationService.Value; }
}
+5 -7
View File
@@ -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);
}
/// <summary>
@@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Umbraco.Core.Models.Rdbms;
using umbraco.interfaces;
namespace Umbraco.Core.Sync
{
/// <summary>
/// An <see cref="IServerMessenger"/> that works by storing messages in the database.
/// </summary>
/// <remarks>
/// abstract because it needs to be inherited by a class that will
/// - trigger FlushBatch() when appropriate
/// - trigger Boot() when appropriate
/// - trigger Sync() when appropriate
/// </remarks>
public abstract class BatchedDatabaseServerMessenger : DatabaseServerMessenger
{
protected BatchedDatabaseServerMessenger(ApplicationContext appContext, bool enableDistCalls, DatabaseServerMessengerOptions options)
: base(appContext, enableDistCalls, options)
{
}
protected abstract ICollection<RefreshInstructionEnvelope> GetBatch(bool ensureHttpContext);
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<IServerAddress> servers, ICacheRefresher refresher, MessageType messageType, IEnumerable<object> 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<IServerAddress> servers,
ICacheRefresher refresher,
MessageType messageType,
IEnumerable<object> 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)));
}
}
}
@@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using System.Linq;
using umbraco.interfaces;
namespace Umbraco.Core.Sync
{
/// <summary>
/// An <see cref="IServerMessenger"/> that works by messaging servers via web services.
/// </summary>
/// <remarks>
/// Abstract because it needs to be inherited by a class that will
/// - implement ProcessBatch()
/// - trigger FlushBatch() when appropriate
/// </remarks>
internal abstract class BatchedWebServiceServerMessenger : WebServiceServerMessenger
{
internal BatchedWebServiceServerMessenger()
{
}
internal BatchedWebServiceServerMessenger(string login, string password)
: base(login, password)
{
}
internal BatchedWebServiceServerMessenger(string login, string password, bool useDistributedCalls)
: base(login, password, useDistributedCalls)
{
}
protected BatchedWebServiceServerMessenger(Func<Tuple<string, string>> getLoginAndPassword)
: base(getLoginAndPassword)
{
}
protected abstract ICollection<RefreshInstructionEnvelope> GetBatch(bool ensureHttpContext);
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<IServerAddress> servers, ICacheRefresher refresher, MessageType messageType, IEnumerable<object> 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<IServerAddress> servers,
ICacheRefresher refresher,
MessageType messageType,
IEnumerable<object> 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)));
}
}
}
+3 -6
View File
@@ -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
{
/// <summary>
/// A server registration based on the legacy umbraco xml configuration in umbracoSettings
/// Provides the address of a server based on the Xml configuration.
/// </summary>
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;
}
}
}
}
+12 -30
View File
@@ -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
{
/// <summary>
/// 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.
/// </summary>
internal class ConfigServerRegistrar : IServerRegistrar
{
private readonly IEnumerable<IServer> _servers;
private readonly List<IServerAddress> _addresses;
public ConfigServerRegistrar()
: this(UmbracoConfig.For.UmbracoSettings().DistributedCall.Servers)
{
}
{ }
internal ConfigServerRegistrar(IEnumerable<IServer> servers)
{
_servers = servers;
_addresses = servers == null
? new List<IServerAddress>()
: servers
.Select(x => new ConfigServerAddress(x))
.Cast<IServerAddress>()
.ToList();
}
private List<IServerAddress> _addresses;
public IEnumerable<IServerAddress> Registrations
{
get
{
if (_addresses == null)
{
_addresses = new List<IServerAddress>();
if (_servers != null)
{
foreach (var n in _servers)
{
_addresses.Add(new ConfigServerAddress(n));
}
}
}
return _addresses;
}
get { return _addresses; }
}
}
}
@@ -0,0 +1,388 @@
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
{
/// <summary>
/// An <see cref="IServerMessenger"/> that works by storing messages in the database.
/// </summary>
//
// 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<IServerAddress> 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<IServerAddress> servers,
ICacheRefresher refresher,
MessageType messageType,
IEnumerable<object> 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
/// <summary>
/// Boots the messenger.
/// </summary>
/// <remarks>
/// Thread safety: this is NOT thread safe. Because it is NOT meant to run multi-threaded.
/// Callers MUST ensure thread-safety.
/// </remarks>
protected void Boot()
{
ReadLastSynced();
Initialize();
}
/// <summary>
/// Initializes a server that has never synchronized before.
/// </summary>
/// <remarks>
/// Thread safety: this is NOT thread safe. Because it is NOT meant to run multi-threaded.
/// Callers MUST ensure thread-safety.
/// </remarks>
private void Initialize()
{
if (_lastId < 0) // never synced before
{
// 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<DatabaseServerMessenger>("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<int>("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;
}
/// <summary>
/// Synchronize the server (throttled).
/// </summary>
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<DatabaseServerMessenger>("Syncing from database..."))
{
ProcessDatabaseInstructions();
PruneOldInstructions();
}
_syncing = false; // release
}
}
/// <summary>
/// Process instructions from the database.
/// </summary>
/// <remarks>
/// Thread safety: this is NOT thread safe. Because it is NOT meant to run multi-threaded.
/// </remarks>
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<CacheInstructionDto>()
.Where<CacheInstructionDto>(dto => dto.Id > _lastId)
.OrderBy<CacheInstructionDto>(dto => dto.Id);
var dtos = _appContext.DatabaseContext.Database.Fetch<CacheInstructionDto>(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<JArray>(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<DatabaseServerMessenger>("Could not deserialize a distributed cache instruction (\"" + dto.Instructions + "\").", ex);
}
}
if (lastId > 0)
SaveLastSynced(lastId);
}
/// <summary>
/// Remove old instructions from the database.
/// </summary>
private void PruneOldInstructions()
{
_appContext.DatabaseContext.Database.Delete<CacheInstructionDto>("WHERE utcStamp < @pruneDate",
new { pruneDate = DateTime.UtcNow.AddDays(-_options.DaysToRetainInstructions) });
}
/// <summary>
/// Reads the last-synced id from file into memory.
/// </summary>
/// <remarks>
/// Thread safety: this is NOT thread safe. Because it is NOT meant to run multi-threaded.
/// </remarks>
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;
}
/// <summary>
/// Updates the in-memory last-synced id and persists it to file.
/// </summary>
/// <param name="id">The id.</param>
/// <remarks>
/// Thread safety: this is NOT thread safe. Because it is NOT meant to run multi-threaded.
/// </remarks>
private void SaveLastSynced(int id)
{
File.WriteAllText(SyncFilePath, id.ToString(CultureInfo.InvariantCulture));
_lastId = id;
}
/// <summary>
/// Gets the local server unique identity.
/// </summary>
/// <returns>The unique identity of the local server.</returns>
protected string GetLocalIdentity()
{
return JsonConvert.SerializeObject(new
{
machineName = NetworkHelper.MachineName,
appDomainAppId = HttpRuntime.AppDomainAppId
});
}
/// <summary>
/// Gets the sync file path for the local server.
/// </summary>
/// <returns>The sync file path for the local server.</returns>
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<JToken> 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<RefreshInstruction>();
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<int[]>(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
}
}
@@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
namespace Umbraco.Core.Sync
{
/// <summary>
/// Provides options to the <see cref="DatabaseServerMessenger"/>.
/// </summary>
public class DatabaseServerMessengerOptions
{
/// <summary>
/// Initializes a new instance of the <see cref="DatabaseServerMessengerOptions"/> with default values.
/// </summary>
public DatabaseServerMessengerOptions()
{
DaysToRetainInstructions = 100; // 100 days
ThrottleSeconds = 5; // 5 seconds
}
/// <summary>
/// A list of callbacks that will be invoked if the lastsynced.txt file does not exist.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public IEnumerable<Action> InitializingCallbacks { get; set; }
/// <summary>
/// The number of days to keep instructions in the database; records older than this number will be pruned.
/// </summary>
public int DaysToRetainInstructions { get; set; }
/// <summary>
/// The number of seconds to wait between each sync operations.
/// </summary>
public int ThrottleSeconds { get; set; }
}
}
@@ -4,19 +4,35 @@ using Umbraco.Core.Services;
namespace Umbraco.Core.Sync
{
/// <summary>
/// A registrar that stores registered server nodes in a database
/// A registrar that stores registered server nodes in the database.
/// </summary>
internal class DatabaseServerRegistrar : IServerRegistrar
public sealed class DatabaseServerRegistrar : IServerRegistrar
{
private readonly Lazy<ServerRegistrationService> _registrationService;
public DatabaseServerRegistrar(Lazy<ServerRegistrationService> registrationService)
/// <summary>
/// Gets or sets the registrar options.
/// </summary>
public DatabaseServerRegistrarOptions Options { get; private set; }
/// <summary>
/// Initializes a new instance of the <see cref="DatabaseServerRegistrar"/> class.
/// </summary>
/// <param name="registrationService">The registration service.</param>
/// <param name="options">Some options.</param>
public DatabaseServerRegistrar(Lazy<ServerRegistrationService> registrationService, DatabaseServerRegistrarOptions options)
{
if (registrationService == null) throw new ArgumentNullException("registrationService");
if (options == null) throw new ArgumentNullException("options");
Options = options;
_registrationService = registrationService;
}
/// <summary>
/// Gets the registered servers.
/// </summary>
public IEnumerable<IServerAddress> Registrations
{
get { return _registrationService.Value.GetActiveServers(); }
@@ -0,0 +1,29 @@
using System;
namespace Umbraco.Core.Sync
{
/// <summary>
/// Provides options to the <see cref="DatabaseServerRegistrar"/>.
/// </summary>
public sealed class DatabaseServerRegistrarOptions
{
/// <summary>
/// Initializes a new instance of the <see cref="DatabaseServerRegistrarOptions"/> class with default values.
/// </summary>
public DatabaseServerRegistrarOptions()
{
StaleServerTimeout = new TimeSpan(1,0,0); // 1 day
ThrottleSeconds = 30; // 30 seconds
}
/// <summary>
/// The number of seconds to wait between each updates to the database.
/// </summary>
public int ThrottleSeconds { get; set; }
/// <summary>
/// The time span to wait before considering a server stale, after it has last been accessed.
/// </summary>
public TimeSpan StaleServerTimeout { get; set; }
}
}
@@ -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
{
/// <summary>
/// The default server messenger that uses web services to keep servers in sync
/// </summary>
internal class DefaultServerMessenger : IServerMessenger
{
private readonly Func<Tuple<string, string>> _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; }
/// <summary>
/// Without a username/password all distribuion will be disabled
/// </summary>
internal DefaultServerMessenger()
{
UseDistributedCalls = false;
}
/// <summary>
/// Distribution will be enabled based on the umbraco config setting.
/// </summary>
/// <param name="login"></param>
/// <param name="password"></param>
internal DefaultServerMessenger(string login, string password)
: this(login, password, UmbracoConfig.For.UmbracoSettings().DistributedCall.Enabled)
{
}
/// <summary>
/// Specifies the username/password and whether or not to use distributed calls
/// </summary>
/// <param name="login"></param>
/// <param name="password"></param>
/// <param name="useDistributedCalls"></param>
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;
}
/// <summary>
/// Allows to set a lazy delegate to resolve the username/password
/// </summary>
/// <param name="getUserNamePasswordDelegate"></param>
public DefaultServerMessenger(Func<Tuple<string, string>> getUserNamePasswordDelegate)
{
_getUserNamePasswordDelegate = getUserNamePasswordDelegate;
}
public void PerformRefresh(IEnumerable<IServerAddress> 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<T>(IEnumerable<IServerAddress> servers, ICacheRefresher refresher,Func<T, int> 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<T>(IEnumerable<IServerAddress> servers, ICacheRefresher refresher, Func<T, Guid> 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<T>(IEnumerable<IServerAddress> servers, ICacheRefresher refresher, Func<T, int> 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<IServerAddress> 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<object>());
}
public void PerformRefresh(IEnumerable<IServerAddress> 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<object>());
}
public void PerformRefresh(IEnumerable<IServerAddress> 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<object>());
}
public void PerformRefreshAll(IEnumerable<IServerAddress> servers, ICacheRefresher refresher)
{
MessageSeversForIdsOrJson(servers, refresher, MessageType.RefreshAll, Enumerable.Empty<object>().ToArray());
}
private void InvokeMethodOnRefresherInstance<T>(ICacheRefresher refresher, MessageType dispatchType, Func<T, object> getId, IEnumerable<T> instances)
{
if (refresher == null) throw new ArgumentNullException("refresher");
LogHelper.Debug<DefaultServerMessenger>("Invoking refresher {0} on single server instance, message type {1}",
() => refresher.GetType(),
() => dispatchType);
var stronglyTypedRefresher = refresher as ICacheRefresher<T>;
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;
}
}
}
/// <summary>
/// If we are instantiated with a lazy delegate to get the username/password, we'll resolve it here
/// </summary>
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<DefaultServerMessenger>("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<object> ids = null, string jsonPayload = null)
{
if (refresher == null) throw new ArgumentNullException("refresher");
LogHelper.Debug<DefaultServerMessenger>("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<T>(
IEnumerable<IServerAddress> servers,
ICacheRefresher refresher,
MessageType dispatchType,
Func<T, object> getId,
IEnumerable<T> 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<IServerAddress> servers,
ICacheRefresher refresher,
MessageType dispatchType,
IEnumerable<object> 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<DefaultServerMessenger>(
"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<object> 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<IServerAddress> servers,
ICacheRefresher refresher,
MessageType dispatchType,
IEnumerable<object> 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<IAsyncResult>();
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<int>().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<DefaultServerMessenger>("Error refreshing distributed list", ee);
}
private void LogDispatchBatchResult(int errorCount)
{
LogHelper.Debug<DefaultServerMessenger>(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<DefaultServerMessenger>("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<DefaultServerMessenger>("Error refreshing a node in the distributed list, URI attempted: " + url, ex);
}
private void LogStartDispatch()
{
LogHelper.Info<DefaultServerMessenger>("Submitting calls to distributed servers");
}
}
}
+4 -1
View File
@@ -3,10 +3,13 @@
namespace Umbraco.Core.Sync
{
/// <summary>
/// An interface exposing a server address to use for server syncing
/// Provides the address of a server.
/// </summary>
public interface IServerAddress
{
/// <summary>
/// Gets the server address.
/// </summary>
string ServerAddress { get; }
//TODO : Should probably add things like port, protocol, server name, app id
+59 -43
View File
@@ -5,81 +5,97 @@ using umbraco.interfaces;
namespace Umbraco.Core.Sync
{
/// <summary>
/// Defines a server messenger for server sync and distrubuted cache
/// Broadcasts distributed cache notifications to all servers of a load balanced environment.
/// </summary>
/// <remarks>Also ensures that the notification is processed on the local environment.</remarks>
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
/// <summary>
/// Performs a refresh and sends along the JSON payload to each server
/// Notifies the distributed cache, for a specified <see cref="ICacheRefresher"/>.
/// </summary>
/// <param name="servers"></param>
/// <param name="refresher"></param>
/// <param name="jsonPayload">
/// A pre-formatted custom json payload to be sent to the servers, the cache refresher will deserialize and use to refresh cache
/// </param>
/// <param name="servers">The servers that compose the load balanced environment.</param>
/// <param name="refresher">The ICacheRefresher.</param>
/// <param name="jsonPayload">The notification content.</param>
void PerformRefresh(IEnumerable<IServerAddress> servers, ICacheRefresher refresher, string jsonPayload);
///// <summary>
///// Notifies the distributed cache, for a specified <see cref="ICacheRefresher"/>.
///// </summary>
///// <param name="servers">The servers that compose the load balanced environment.</param>
///// <param name="refresher">The ICacheRefresher.</param>
///// <param name="payload">The notification content.</param>
///// <param name="serializer">A custom Json serializer.</param>
//void Notify(IEnumerable<IServerAddress> servers, ICacheRefresher refresher, object payload, Func<object, string> serializer = null);
/// <summary>
/// Performs a sync against all instance objects
/// Notifies the distributed cache of specifieds item invalidation, for a specified <see cref="ICacheRefresher"/>.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="servers">The servers to sync against</param>
/// <param name="refresher"></param>
/// <param name="getNumericId">A delegate to return the Id for each instance to be used to sync to other servers</param>
/// <param name="instances"></param>
/// <typeparam name="T">The type of the invalidated items.</typeparam>
/// <param name="servers">The servers that compose the load balanced environment.</param>
/// <param name="refresher">The ICacheRefresher.</param>
/// <param name="getNumericId">A function returning the unique identifier of items.</param>
/// <param name="instances">The invalidated items.</param>
void PerformRefresh<T>(IEnumerable<IServerAddress> servers, ICacheRefresher refresher, Func<T, int> getNumericId, params T[] instances);
/// <summary>
/// Performs a sync against all instance objects
/// Notifies the distributed cache of specifieds item invalidation, for a specified <see cref="ICacheRefresher"/>.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="servers">The servers to sync against</param>
/// <param name="refresher"></param>
/// <param name="getGuidId">A delegate to return the Id for each instance to be used to sync to other servers</param>
/// <param name="instances"></param>
/// <typeparam name="T">The type of the invalidated items.</typeparam>
/// <param name="servers">The servers that compose the load balanced environment.</param>
/// <param name="refresher">The ICacheRefresher.</param>
/// <param name="getGuidId">A function returning the unique identifier of items.</param>
/// <param name="instances">The invalidated items.</param>
void PerformRefresh<T>(IEnumerable<IServerAddress> servers, ICacheRefresher refresher, Func<T, Guid> getGuidId, params T[] instances);
/// <summary>
/// Removes the cache for the specified items
/// Notifies all servers of specified items removal, for a specified <see cref="ICacheRefresher"/>.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="servers"></param>
/// <param name="refresher"></param>
/// <param name="getNumericId">A delegate to return the Id for each instance to be used to sync to other servers</param>
/// <param name="instances"></param>
/// <typeparam name="T">The type of the removed items.</typeparam>
/// <param name="servers">The servers that compose the load balanced environment.</param>
/// <param name="refresher">The ICacheRefresher.</param>
/// <param name="getNumericId">A function returning the unique identifier of items.</param>
/// <param name="instances">The removed items.</param>
void PerformRemove<T>(IEnumerable<IServerAddress> servers, ICacheRefresher refresher, Func<T, int> getNumericId, params T[] instances);
/// <summary>
/// Removes the cache for the specified items
/// Notifies all servers of specified items removal, for a specified <see cref="ICacheRefresher"/>.
/// </summary>
/// <param name="servers"></param>
/// <param name="refresher"></param>
/// <param name="numericIds"></param>
/// <param name="servers">The servers that compose the load balanced environment.</param>
/// <param name="refresher">The ICacheRefresher.</param>
/// <param name="numericIds">The unique identifiers of the removed items.</param>
void PerformRemove(IEnumerable<IServerAddress> servers, ICacheRefresher refresher, params int[] numericIds);
/// <summary>
/// Performs a sync against all Ids
/// Notifies all servers of specified items invalidation, for a specified <see cref="ICacheRefresher"/>.
/// </summary>
/// <param name="servers">The servers to sync against</param>
/// <param name="refresher"></param>
/// <param name="numericIds"></param>
/// <param name="servers">The servers that compose the load balanced environment.</param>
/// <param name="refresher">The ICacheRefresher.</param>
/// <param name="numericIds">The unique identifiers of the invalidated items.</param>
void PerformRefresh(IEnumerable<IServerAddress> servers, ICacheRefresher refresher, params int[] numericIds);
/// <summary>
/// Performs a sync against all Ids
/// Notifies all servers of specified items invalidation, for a specified <see cref="ICacheRefresher"/>.
/// </summary>
/// <param name="servers">The servers to sync against</param>
/// <param name="refresher"></param>
/// <param name="guidIds"></param>
/// <param name="servers">The servers that compose the load balanced environment.</param>
/// <param name="refresher">The ICacheRefresher.</param>
/// <param name="guidIds">The unique identifiers of the invalidated items.</param>
void PerformRefresh(IEnumerable<IServerAddress> servers, ICacheRefresher refresher, params Guid[] guidIds);
/// <summary>
/// Performs entire cache refresh for a specified refresher
/// Notifies all servers of a global invalidation for a specified <see cref="ICacheRefresher"/>.
/// </summary>
/// <param name="servers"></param>
/// <param name="refresher"></param>
/// <param name="servers">The servers that compose the load balanced environment.</param>
/// <param name="refresher">The ICacheRefresher.</param>
void PerformRefreshAll(IEnumerable<IServerAddress> servers, ICacheRefresher refresher);
}
}
+4 -1
View File
@@ -3,10 +3,13 @@
namespace Umbraco.Core.Sync
{
/// <summary>
/// An interface to expose a list of server registrations for server syncing
/// Provides server registrations to the distributed cache.
/// </summary>
public interface IServerRegistrar
{
/// <summary>
/// Gets the server registrations.
/// </summary>
IEnumerable<IServerAddress> Registrations { get; }
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
namespace Umbraco.Core.Sync
{
/// <summary>
/// The message type to be used for syncing across servers
/// The message type to be used for syncing across servers.
/// </summary>
public enum MessageType
{
+125 -22
View File
@@ -1,46 +1,149 @@
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
// need the public one so it can be de-serialized
// otherwise, should use GetInstructions(...)
public RefreshInstruction(Guid refresherId, RefreshMethodType refreshType, Guid guidId, int intId, string jsonIds, string jsonPayload)
{
RefreshAll,
RefreshByGuid,
RefreshById,
RefreshByIds,
RefreshByJson,
RemoveById
RefresherId = refresherId;
RefreshType = refreshType;
GuidId = guidId;
IntId = intId;
JsonIds = jsonIds;
JsonPayload = jsonPayload;
}
private RefreshInstruction(ICacheRefresher refresher, RefreshMethodType refreshType)
{
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<RefreshInstruction> GetInstructions(
ICacheRefresher refresher,
MessageType messageType,
IEnumerable<object> 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<int>().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<int>().ToArray())) };
default:
//case MessageType.RefreshByInstance:
//case MessageType.RemoveByInstance:
throw new ArgumentOutOfRangeException("messageType");
}
}
/// <summary>
/// Gets or sets the refresh action type.
/// </summary>
public RefreshMethodType RefreshType { get; set; }
/// <summary>
/// Gets or sets the refresher unique identifier.
/// </summary>
public Guid RefresherId { get; set; }
/// <summary>
/// Gets or sets the Guid data value.
/// </summary>
public Guid GuidId { get; set; }
/// <summary>
/// Gets or sets the int data value.
/// </summary>
public int IntId { get; set; }
/// <summary>
/// Gets or sets the ids data value.
/// </summary>
public string JsonIds { get; set; }
/// <summary>
/// Gets or sets the payload data value.
/// </summary>
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 +160,7 @@ namespace Umbraco.Core.Sync
public static bool operator !=(RefreshInstruction left, RefreshInstruction right)
{
return !Equals(left, right);
return Equals(left, right) == false;
}
}
}
@@ -0,0 +1,23 @@
using System.Collections.Generic;
using umbraco.interfaces;
namespace Umbraco.Core.Sync
{
/// <summary>
/// Used for any 'Batched' <see cref="IServerMessenger"/> instances which specifies a set of <see cref="RefreshInstruction"/> targeting a collection of
/// <see cref="IServerAddress"/>
/// </summary>
public sealed class RefreshInstructionEnvelope
{
public RefreshInstructionEnvelope(IEnumerable<IServerAddress> servers, ICacheRefresher refresher, IEnumerable<RefreshInstruction> instructions)
{
Servers = servers;
Refresher = refresher;
Instructions = instructions;
}
public IEnumerable<IServerAddress> Servers { get; set; }
public ICacheRefresher Refresher { get; set; }
public IEnumerable<RefreshInstruction> Instructions { get; set; }
}
}
@@ -0,0 +1,44 @@
using System;
namespace Umbraco.Core.Sync
{
/// <summary>
/// Describes <see cref="RefreshInstruction"/> refresh action type.
/// </summary>
[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)
}
}
@@ -0,0 +1,338 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Umbraco.Core.Cache;
using Umbraco.Core.Logging;
using umbraco.interfaces;
namespace Umbraco.Core.Sync
{
/// <summary>
/// Provides a base class for all <see cref="IServerMessenger"/> implementations.
/// </summary>
public abstract class ServerMessengerBase : IServerMessenger
{
protected bool DistributedEnabled { get; set; }
protected ServerMessengerBase(bool distributedEnabled)
{
DistributedEnabled = distributedEnabled;
}
/// <summary>
/// Determines whether to make distributed calls when messaging a cache refresher.
/// </summary>
/// <param name="servers">The registered servers.</param>
/// <param name="refresher">The cache refresher.</param>
/// <param name="messageType">The message type.</param>
/// <returns>true if distributed calls are required; otherwise, false, all we have is the local server.</returns>
protected virtual bool RequiresDistributed(IEnumerable<IServerAddress> 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<object> 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<IServerAddress> 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<T>(IEnumerable<IServerAddress> servers, ICacheRefresher refresher, Func<T, int> 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<T, object> getId = x => getNumericId(x);
Deliver(servers, refresher, MessageType.RefreshByInstance, getId, instances);
}
public void PerformRefresh<T>(IEnumerable<IServerAddress> servers, ICacheRefresher refresher, Func<T, Guid> 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<T, object> getId = x => getGuidId(x);
Deliver(servers, refresher, MessageType.RefreshByInstance, getId, instances);
}
public void PerformRemove<T>(IEnumerable<IServerAddress> servers, ICacheRefresher refresher, Func<T, int> 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<T, object> getId = x => getNumericId(x);
Deliver(servers, refresher, MessageType.RemoveByInstance, getId, instances);
}
public void PerformRemove(IEnumerable<IServerAddress> 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<object>());
}
public void PerformRefresh(IEnumerable<IServerAddress> 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<object>());
}
public void PerformRefresh(IEnumerable<IServerAddress> 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<object>());
}
public void PerformRefreshAll(IEnumerable<IServerAddress> 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<IServerAddress> 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
/// <summary>
/// Executes the non strongly typed <see cref="ICacheRefresher"/> on the local/current server
/// </summary>
/// <param name="refresher"></param>
/// <param name="messageType"></param>
/// <param name="ids"></param>
/// <param name="json"></param>
/// <remarks>
/// Since this is only for non strongly typed <see cref="ICacheRefresher"/> it will throw for message types that by instance
/// </remarks>
protected void DeliverLocal(ICacheRefresher refresher, MessageType messageType, IEnumerable<object> ids = null, string json = null)
{
if (refresher == null) throw new ArgumentNullException("refresher");
LogHelper.Debug<ServerMessengerBase>("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);
}
}
/// <summary>
/// Executes the strongly typed <see cref="ICacheRefresher{T}"/> on the local/current server
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="refresher"></param>
/// <param name="messageType"></param>
/// <param name="getId"></param>
/// <param name="instances"></param>
/// <remarks>
/// Since this is only for strongly typed <see cref="ICacheRefresher{T}"/> it will throw for message types that are not by instance
/// </remarks>
protected void DeliverLocal<T>(ICacheRefresher refresher, MessageType messageType, Func<T, object> getId, IEnumerable<T> instances)
{
if (refresher == null) throw new ArgumentNullException("refresher");
LogHelper.Debug<ServerMessengerBase>("Invoking refresher {0} on local server for message type {1}",
refresher.GetType,
() => messageType);
var typedRefresher = refresher as ICacheRefresher<T>;
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<ServerMessengerBase>("Invoking refresher {0} on local server for message type Notify",
// () => refresher.GetType());
// refresher.Notify(payload);
//}
protected abstract void DeliverRemote(IEnumerable<IServerAddress> servers, ICacheRefresher refresher, MessageType messageType, IEnumerable<object> ids = null, string json = null);
//protected abstract void DeliverRemote(IEnumerable<IServerAddress> servers, ICacheRefresher refresher, object payload);
protected virtual void Deliver(IEnumerable<IServerAddress> servers, ICacheRefresher refresher, MessageType messageType, IEnumerable<object> 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<T>(IEnumerable<IServerAddress> servers, ICacheRefresher refresher, MessageType messageType, Func<T, object> getId, IEnumerable<T> 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<IServerAddress> 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
}
}
@@ -3,24 +3,31 @@
namespace Umbraco.Core.Sync
{
/// <summary>
/// A resolver to return the currently registered IServerMessenger object
/// Resolves the IServerMessenger object.
/// </summary>
public sealed class ServerMessengerResolver : SingleObjectResolverBase<ServerMessengerResolver, IServerMessenger>
{
/// <summary>
/// Initializes a new instance of the <see cref="ServerMessengerResolver"/> class with a messenger.
/// </summary>
/// <param name="factory">An instance of a messenger.</param>
/// <remarks>The resolver is created by the <c>CoreBootManager</c> and thus the constructor remains internal.</remarks>
internal ServerMessengerResolver(IServerMessenger factory)
: base(factory)
{
}
{ }
/// <summary>
/// Can be used at runtime to set a custom IServerMessenger at app startup
/// Sets the messenger.
/// </summary>
/// <param name="serverMessenger"></param>
/// <param name="serverMessenger">The messenger.</param>
public void SetServerMessenger(IServerMessenger serverMessenger)
{
Value = serverMessenger;
}
/// <summary>
/// Gets the messenger.
/// </summary>
public IServerMessenger Messenger
{
get { return Value; }
@@ -3,25 +3,32 @@
namespace Umbraco.Core.Sync
{
/// <summary>
/// The resolver to return the currently registered IServerRegistrar object
/// Resolves the IServerRegistrar object.
/// </summary>
public sealed class ServerRegistrarResolver : SingleObjectResolverBase<ServerRegistrarResolver, IServerRegistrar>
{
/// <summary>
/// Initializes a new instance of the <see cref="ServerRegistrarResolver"/> class with a registrar.
/// </summary>
/// <param name="factory">An instance of a registrar.</param>
/// <remarks>The resolver is created by the <c>CoreBootManager</c> and thus the constructor remains internal.</remarks>
internal ServerRegistrarResolver(IServerRegistrar factory)
: base(factory)
{
}
{ }
/// <summary>
/// Can be used at runtime to set a custom IServerRegistrar at app startup
/// Sets the registrar.
/// </summary>
/// <param name="serverRegistrar"></param>
/// <param name="serverRegistrar">The registrar.</param>
/// <remarks>For developers, at application startup.</remarks>
public void SetServerRegistrar(IServerRegistrar serverRegistrar)
{
Value = serverRegistrar;
}
/// <summary>
/// Gets the registrar.
/// </summary>
public IServerRegistrar Registrar
{
get { return Value; }
@@ -0,0 +1,385 @@
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
{
/// <summary>
/// An <see cref="IServerMessenger"/> that works by messaging servers via web services.
/// </summary>
/// <remarks>
/// 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
/// </remarks>
//
// 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<Tuple<string, string>> _getLoginAndPassword;
private volatile bool _hasLoginAndPassword;
private readonly object _locker = new object();
protected string Login { get; private set; }
protected string Password{ get; private set; }
/// <summary>
/// Initializes a new instance of the <see cref="WebServiceServerMessenger"/> class.
/// </summary>
/// <remarks>Distribution is disabled.</remarks>
internal WebServiceServerMessenger()
: base(false)
{ }
/// <summary>
/// Initializes a new instance of the <see cref="WebServiceServerMessenger"/> class with a login and a password.
/// </summary>
/// <param name="login">The login.</param>
/// <param name="password">The password.</param>
/// <remarks>Distribution will be enabled based on the umbraco config setting.</remarks>
internal WebServiceServerMessenger(string login, string password)
: this(login, password, UmbracoConfig.For.UmbracoSettings().DistributedCall.Enabled)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="WebServiceServerMessenger"/> class with a login and a password
/// and a value indicating whether distribution is enabled.
/// </summary>
/// <param name="login">The login.</param>
/// <param name="password">The password.</param>
/// <param name="distributedEnabled">A value indicating whether distribution is enabled.</param>
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;
}
/// <summary>
/// Initializes a new instance of the <see cref="WebServiceServerMessenger"/> with a function providing
/// a login and a password.
/// </summary>
/// <param name="getLoginAndPassword">A function providing a login and a password.</param>
/// <remarks>Distribution will be enabled based on the umbraco config setting.</remarks>
public WebServiceServerMessenger(Func<Tuple<string, string>> 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<WebServiceServerMessenger>("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(appDomainAppId);
hasher.AddCaseInsensitiveString(machineName);
return hasher.GetCombinedHashCode();
}
protected override bool RequiresDistributed(IEnumerable<IServerAddress> servers, ICacheRefresher refresher, MessageType messageType)
{
EnsureLoginAndPassword();
return base.RequiresDistributed(servers, refresher, messageType);
}
protected override void DeliverRemote(IEnumerable<IServerAddress> servers, ICacheRefresher refresher, MessageType messageType, IEnumerable<object> 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<IServerAddress> servers,
ICacheRefresher refresher,
MessageType messageType,
IEnumerable<object> ids = null,
Type idArrayType = null,
string jsonPayload = null)
{
LogHelper.Debug<WebServiceServerMessenger>(
"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<IAsyncResult>();
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<int>().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<RefreshInstructionEnvelope> 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<IAsyncResult>();
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<WebServiceServerMessenger>("Error refreshing distributed list", ee);
}
private static void LogDispatchBatchResult(int errorCount)
{
LogHelper.Debug<WebServiceServerMessenger>(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<WebServiceServerMessenger>("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<WebServiceServerMessenger>("Error refreshing a node in the distributed list, URI attempted: " + url, ex);
}
private static void LogStartDispatch()
{
LogHelper.Info<WebServiceServerMessenger>("Submitting calls to distributed servers");
}
#endregion
}
}
+12 -1
View File
@@ -385,9 +385,11 @@
<Compile Include="IO\ResizedImage.cs" />
<Compile Include="IO\UmbracoMediaFile.cs" />
<Compile Include="Logging\DebugDiagnosticsLogger.cs" />
<Compile Include="Logging\AppDomainTokenFormatter.cs" />
<Compile Include="Logging\LoggerExtensions.cs" />
<Compile Include="Logging\LoggerResolver.cs" />
<Compile Include="Logging\ProfilingLogger.cs" />
<Compile Include="Persistence\Migrations\Upgrades\TargetVersionSevenThreeZero\CreateCacheInstructionTable.cs" />
<Compile Include="Persistence\Migrations\Upgrades\TargetVersionSevenThreeZero\MigrateStylesheetDataToFile.cs" />
<Compile Include="Services\AuditService.cs" />
<Compile Include="Services\DomainService.cs" />
@@ -458,6 +460,7 @@
<Compile Include="Models\PublishedContent\PublishedContentModel.cs" />
<Compile Include="Models\PublishedContent\PublishedContentModelFactoryResolver.cs" />
<Compile Include="Models\PartialView.cs" />
<Compile Include="Models\Rdbms\CacheInstructionDto.cs" />
<Compile Include="Models\TagCacheStorageType.cs" />
<Compile Include="Models\TaggableObjectTypes.cs" />
<Compile Include="Models\TaggedEntity.cs" />
@@ -1175,15 +1178,23 @@
<Compile Include="Strategies\RelateOnTrashHandler.cs" />
<Compile Include="Strings\ContentBaseExtensions.cs" />
<Compile Include="Strings\Diff.cs" />
<Compile Include="Sync\BatchedDatabaseServerMessenger.cs" />
<Compile Include="Sync\BatchedWebServiceServerMessenger.cs" />
<Compile Include="Sync\CurrentServerEnvironmentStatus.cs" />
<Compile Include="Sync\DatabaseServerMessenger.cs" />
<Compile Include="Sync\DatabaseServerMessengerOptions.cs" />
<Compile Include="Sync\DatabaseServerRegistrarOptions.cs" />
<Compile Include="Sync\RefreshInstruction.cs" />
<Compile Include="Sync\RefreshInstructionEnvelope.cs" />
<Compile Include="Sync\ServerEnvironmentHelper.cs" />
<Compile Include="Sync\RefreshMethodType.cs" />
<Compile Include="Sync\ServerMessengerBase.cs" />
<Compile Include="TopologicalSorter.cs" />
<Compile Include="Strings\DefaultUrlSegmentProvider.cs" />
<Compile Include="Strings\IUrlSegmentProvider.cs" />
<Compile Include="Strings\UrlSegmentProviderResolver.cs" />
<Compile Include="Sync\DatabaseServerRegistrar.cs" />
<Compile Include="Sync\DefaultServerMessenger.cs" />
<Compile Include="Sync\WebServiceServerMessenger.cs" />
<Compile Include="Cache\ICacheRefresher.cs" />
<Compile Include="Sync\ServerSyncWebServiceClient.cs">
<SubType>Component</SubType>
+15 -5
View File
@@ -1,4 +1,6 @@
using System;
using NUnit.Framework;
using Umbraco.Core.Sync;
using umbraco.presentation.webservices;
namespace Umbraco.Tests.Cache
@@ -8,14 +10,22 @@ namespace Umbraco.Tests.Cache
{
[TestCase("", "123456", "testmachine", true)] //empty hash will continue
[TestCase("fffffff28449cf33", "123456", "testmachine", false)] //match, don't continue
[TestCase("fffffff28449cf33", "12345", "testmachine", true)]
[TestCase("fffffff28449cf33", "123456", "testmachin", true)]
[TestCase("fffffff28449cf3", "123456", "testmachine", true)]
[TestCase("fffffff28449cf33", "12345", "testmachine", true)] // no match, continue
[TestCase("fffffff28449cf33", "123456", "testmachin", true)] // same
[TestCase("fffffff28449cf3", "123456", "testmachine", true)] // same
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.IsTrue(Continue(hash, WebServiceServerMessenger.GetServerHash(appDomainAppId, machineName)));
else
Assert.IsFalse(Continue(hash, WebServiceServerMessenger.GetServerHash(appDomainAppId, machineName)));
}
// that's what CacheRefresher.asmx.cs does...
private bool Continue(string hash1, string hash2)
{
if (string.IsNullOrEmpty(hash1)) return true;
return hash1 != hash2;
}
}
}
@@ -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();
@@ -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<SqlCeException>(unitOfWork.Commit);
}
@@ -128,7 +128,7 @@ namespace Umbraco.Tests.Persistence.Repositories
using (var repository = CreateRepositor(unitOfWork))
{
// Act
var query = Query<ServerRegistration>.Builder.Where(x => x.ComputerName.ToUpper() == "COMPUTER3");
var query = Query<ServerRegistration>.Builder.Where(x => x.ServerIdentity.ToUpper() == "COMPUTER3");
var result = repository.GetByQuery(query);
// Assert
+1
View File
@@ -209,6 +209,7 @@
</Reference>
<Reference Include="System.Net.Http.Formatting, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.AspNet.WebApi.Client.4.0.30506.0\lib\net40\System.Net.Http.Formatting.dll</HintPath>
<SpecificVersion>False</SpecificVersion>
<Private>True</Private>
</Reference>
<Reference Include="System.Net.Http.Primitives">
@@ -0,0 +1,88 @@
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
{
/// <summary>
/// An <see cref="IServerMessenger"/> implementation that works by storing messages in the database.
/// </summary>
/// <remarks>
/// This binds to appropriate umbraco events in order to trigger the Boot(), Sync() & FlushBatch() calls
/// </remarks>
public class BatchedDatabaseServerMessenger : Core.Sync.BatchedDatabaseServerMessenger
{
public BatchedDatabaseServerMessenger(ApplicationContext appContext, bool enableDistCalls, DatabaseServerMessengerOptions options)
: base(appContext, enableDistCalls, options)
{
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<BatchedDatabaseServerMessenger>("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();
}
protected override ICollection<RefreshInstructionEnvelope> GetBatch(bool ensureHttpContext)
{
var httpContext = UmbracoContext.Current == null ? null : UmbracoContext.Current.HttpContext;
if (httpContext == null)
{
if (ensureHttpContext)
throw new NotSupportedException("Cannot execute without a valid/current UmbracoContext with an HttpContext assigned.");
return null;
}
var key = typeof (BatchedDatabaseServerMessenger).Name;
// no thread-safety here because it'll run in only 1 thread (request) at a time
var batch = (ICollection<RefreshInstructionEnvelope>)httpContext.Items[key];
if (batch == null && ensureHttpContext)
httpContext.Items[key] = batch = new List<RefreshInstructionEnvelope>();
return batch;
}
}
}
-326
View File
@@ -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<Tuple<string, string>> 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<Message>;
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<IServerAddress> Servers { get; set; }
public ICacheRefresher Refresher { get; set; }
public MessageType DispatchType { get; set; }
public IEnumerable<object> Ids { get; set; }
public Type IdArrayType { get; set; }
public string JsonPayload { get; set; }
}
/// <summary>
/// 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.
/// </summary>
/// <param name="servers"></param>
/// <param name="refresher"></param>
/// <param name="dispatchType"></param>
/// <param name="ids"></param>
/// <param name="jsonPayload"></param>
/// <remarks>
/// See: http://issues.umbraco.org/issue/U4-2633#comment=67-15604
/// </remarks>
protected override void MessageSeversForIdsOrJson(IEnumerable<IServerAddress> servers, ICacheRefresher refresher, MessageType dispatchType, IEnumerable<object> 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);
}
}
/// <summary>
/// This adds the call to batched list
/// </summary>
/// <param name="servers"></param>
/// <param name="refresher"></param>
/// <param name="dispatchType"></param>
/// <param name="ids"></param>
/// <param name="idArrayType"></param>
/// <param name="jsonPayload"></param>
protected override void PerformDistributedCall(
IEnumerable<IServerAddress> servers,
ICacheRefresher refresher,
MessageType dispatchType,
IEnumerable<object> 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<Message>();
}
var list = (List<Message>)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<int>().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<Message> messages)
{
var batchedMsg = new List<Tuple<Message, RefreshInstruction[]>>();
foreach (var msg in messages)
{
var instructions = ConvertToInstruction(msg);
batchedMsg.Add(new Tuple<Message, RefreshInstruction[]>(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<IAsyncResult>();
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<BatchedServerMessenger>("Error refreshing distributed list", ee);
}
private void LogDispatchBatchResult(int errorCount)
{
LogHelper.Debug<BatchedServerMessenger>(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<BatchedServerMessenger>("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<BatchedServerMessenger>("Error refreshing a node in the distributed list, URI attempted: " + url, ex);
}
private void LogStartDispatch()
{
LogHelper.Info<BatchedServerMessenger>("Submitting calls to distributed servers");
}
}
}
@@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
using Umbraco.Core.Sync;
namespace Umbraco.Web
{
/// <summary>
/// An <see cref="IServerMessenger"/> that works by messaging servers via web services.
/// </summary>
/// <remarks>
/// This binds to appropriate umbraco events in order to trigger the FlushBatch() calls
/// </remarks>
internal class BatchedWebServiceServerMessenger : Core.Sync.BatchedWebServiceServerMessenger
{
internal BatchedWebServiceServerMessenger()
: base()
{
UmbracoModule.EndRequest += UmbracoModule_EndRequest;
}
internal BatchedWebServiceServerMessenger(string login, string password)
: base(login, password)
{
UmbracoModule.EndRequest += UmbracoModule_EndRequest;
}
internal BatchedWebServiceServerMessenger(string login, string password, bool useDistributedCalls)
: base(login, password, useDistributedCalls)
{
UmbracoModule.EndRequest += UmbracoModule_EndRequest;
}
public BatchedWebServiceServerMessenger(Func<Tuple<string, string>> getLoginAndPassword)
: base(getLoginAndPassword)
{
UmbracoModule.EndRequest += UmbracoModule_EndRequest;
}
protected override ICollection<RefreshInstructionEnvelope> GetBatch(bool ensureHttpContext)
{
var httpContext = UmbracoContext.Current == null ? null : UmbracoContext.Current.HttpContext;
if (httpContext == null)
{
if (ensureHttpContext)
throw new NotSupportedException("Cannot execute without a valid/current UmbracoContext with an HttpContext assigned.");
return null;
}
var key = typeof(BatchedWebServiceServerMessenger).Name;
// no thread-safety here because it'll run in only 1 thread (request) at a time
var batch = (ICollection<RefreshInstructionEnvelope>)httpContext.Items[key];
if (batch == null && ensureHttpContext)
httpContext.Items[key] = batch = new List<RefreshInstructionEnvelope>();
return batch;
}
void UmbracoModule_EndRequest(object sender, EventArgs e)
{
FlushBatch();
}
protected override void ProcessBatch(RefreshInstructionEnvelope[] batch)
{
Message(batch);
}
}
}
+95 -70
View File
@@ -1,38 +1,25 @@
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
//{
//}
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// 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.
/// <para>
/// The distributed cache infrastructure ensures that distributed caches are
/// invalidated properly in load balancing environments.
/// </para>
/// <para>
/// Distribute caches include static (in-memory) cache, runtime cache, front-end content cache, Examine/Lucene indexes
/// </para>
/// </remarks>
public sealed class DistributedCache
{
#region Public constants/Ids
public const string ApplicationTreeCacheRefresherId = "0AC6C028-9860-4EA4-958D-14D39F45886E";
@@ -56,21 +43,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();
/// <summary>
/// Constructor
/// Initializes a new instance of the <see cref="DistributedCache"/> class.
/// </summary>
private DistributedCache()
{
}
{ }
/// <summary>
/// Singleton
/// Gets the static unique instance of the <see cref="DistributedCache"/> class.
/// </summary>
/// <returns></returns>
/// <returns>The static unique instance of the <see cref="DistributedCache"/> class.</returns>
/// <remarks>Exists so that extension methods can be added to the distributed cache.</remarks>
public static DistributedCache Instance
{
get
@@ -79,22 +90,25 @@ namespace Umbraco.Web.Cache
}
}
#endregion
#region Core notification methods
/// <summary>
/// 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 <see cref="ICacheRefresher"/>.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="factoryGuid"></param>
/// <param name="getNumericId">The callback method to retrieve the ID from an instance</param>
/// <param name="instances">The instances containing Ids</param>
/// <typeparam name="T">The type of the invalidated items.</typeparam>
/// <param name="factoryGuid">The unique identifier of the ICacheRefresher.</param>
/// <param name="getNumericId">A function returning the unique identifier of items.</param>
/// <param name="instances">The invalidated items.</param>
/// <remarks>
/// 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.
/// </remarks>
public void Refresh<T>(Guid factoryGuid, Func<T, int> getNumericId, params T[] instances)
{
if (factoryGuid == Guid.Empty || instances.Length == 0 || getNumericId == null) return;
ServerMessengerResolver.Current.Messenger.PerformRefresh<T>(
ServerMessengerResolver.Current.Messenger.PerformRefresh(
ServerRegistrarResolver.Current.Registrar.Registrations,
GetRefresherById(factoryGuid),
getNumericId,
@@ -102,11 +116,10 @@ namespace Umbraco.Web.Cache
}
/// <summary>
/// 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 <see cref="ICacheRefresher"/>.
/// </summary>
/// <param name="factoryGuid">The unique identifier of the ICacheRefresher used to refresh the node.</param>
/// <param name="id">The id of the node.</param>
/// <param name="factoryGuid">The unique identifier of the ICacheRefresher.</param>
/// <param name="id">The unique identifier of the invalidated item.</param>
public void Refresh(Guid factoryGuid, int id)
{
if (factoryGuid == Guid.Empty || id == default(int)) return;
@@ -118,11 +131,10 @@ namespace Umbraco.Web.Cache
}
/// <summary>
/// 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 <see cref="ICacheRefresher"/>.
/// </summary>
/// <param name="factoryGuid">The unique identifier of the ICacheRefresher used to refresh the node.</param>
/// <param name="id">The guid of the node.</param>
/// <param name="factoryGuid">The unique identifier of the ICacheRefresher.</param>
/// <param name="id">The unique identifier of the invalidated item.</param>
public void Refresh(Guid factoryGuid, Guid id)
{
if (factoryGuid == Guid.Empty || id == Guid.Empty) return;
@@ -134,11 +146,10 @@ namespace Umbraco.Web.Cache
}
/// <summary>
/// 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 <see cref="ICacheRefresher"/>.
/// </summary>
/// <param name="factoryGuid"></param>
/// <param name="jsonPayload"></param>
/// <param name="factoryGuid">The unique identifier of the ICacheRefresher.</param>
/// <param name="jsonPayload">The notification content.</param>
public void RefreshByJson(Guid factoryGuid, string jsonPayload)
{
if (factoryGuid == Guid.Empty || jsonPayload.IsNullOrWhiteSpace()) return;
@@ -149,26 +160,37 @@ namespace Umbraco.Web.Cache
jsonPayload);
}
///// <summary>
///// Notifies the distributed cache, for a specified <see cref="ICacheRefresher"/>.
///// </summary>
///// <param name="refresherId">The unique identifier of the ICacheRefresher.</param>
///// <param name="payload">The notification content.</param>
//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);
//}
/// <summary>
/// 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 <see cref="ICacheRefresher"/>.
/// </summary>
/// <param name="factoryGuid">The unique identifier.</param>
/// <param name="factoryGuid">The unique identifier of the ICacheRefresher.</param>
public void RefreshAll(Guid factoryGuid)
{
if (factoryGuid == Guid.Empty) return;
RefreshAll(factoryGuid, true);
}
/// <summary>
/// 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 <see cref="ICacheRefresher"/>.
/// </summary>
/// <param name="factoryGuid">The unique identifier.</param>
/// <param name="allServers">
/// If true will send the request out to all registered LB servers, if false will only execute the current server
/// </param>
/// <param name="factoryGuid">The unique identifier of the ICacheRefresher.</param>
/// <param name="allServers">If true, all servers in the load balancing environment are notified; otherwise,
/// only the local server is notified.</param>
public void RefreshAll(Guid factoryGuid, bool allServers)
{
if (factoryGuid == Guid.Empty) return;
@@ -181,11 +203,10 @@ namespace Umbraco.Web.Cache
}
/// <summary>
/// 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 <see cref="ICacheRefresher"/>.
/// </summary>
/// <param name="factoryGuid">The unique identifier.</param>
/// <param name="id">The id.</param>
/// <param name="factoryGuid">The unique identifier of the ICacheRefresher.</param>
/// <param name="id">The unique identifier of the removed item.</param>
public void Remove(Guid factoryGuid, int id)
{
if (factoryGuid == Guid.Empty || id == default(int)) return;
@@ -195,28 +216,32 @@ namespace Umbraco.Web.Cache
GetRefresherById(factoryGuid),
id);
}
/// <summary>
/// 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 <see cref="ICacheRefresher"/>.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="factoryGuid"></param>
/// <param name="getNumericId"></param>
/// <param name="instances"></param>
/// <typeparam name="T">The type of the removed items.</typeparam>
/// <param name="factoryGuid">The unique identifier of the ICacheRefresher.</param>
/// <param name="getNumericId">A function returning the unique identifier of items.</param>
/// <param name="instances">The removed items.</param>
/// <remarks>
/// This method is much better for performance because it does not need to re-lookup object instances.
/// </remarks>
public void Remove<T>(Guid factoryGuid, Func<T, int> getNumericId, params T[] instances)
{
ServerMessengerResolver.Current.Messenger.PerformRemove<T>(
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);
}
}
}
@@ -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
{
/// <summary>
/// Extension methods for DistrubutedCache
/// Extension methods for <see cref="DistributedCache"/>
/// </summary>
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
/// <summary>
/// Refreshes the cache amongst servers for a template
/// </summary>
/// <param name="dc"></param>
/// <param name="templateId"></param>
public static void RefreshTemplateCache(this DistributedCache dc, int templateId)
{
dc.Refresh(new Guid(DistributedCache.TemplateRefresherId), templateId);
dc.Refresh(DistributedCache.TemplateRefresherGuid, templateId);
}
/// <summary>
/// Removes the cache amongst servers for a template
/// </summary>
/// <param name="dc"></param>
/// <param name="templateId"></param>
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
/// <summary>
/// Refreshes the cache amongst servers for a dictionary item
/// </summary>
/// <param name="dc"></param>
/// <param name="dictionaryItemId"></param>
public static void RefreshDictionaryCache(this DistributedCache dc, int dictionaryItemId)
{
dc.Refresh(new Guid(DistributedCache.DictionaryCacheRefresherId), dictionaryItemId);
dc.Refresh(DistributedCache.DictionaryCacheRefresherGuid, dictionaryItemId);
}
/// <summary>
/// Refreshes the cache amongst servers for a dictionary item
/// </summary>
/// <param name="dc"></param>
/// <param name="dictionaryItemId"></param>
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
/// <summary>
/// Refreshes the cache amongst servers for a data type
/// </summary>
/// <param name="dc"></param>
/// <param name="dataType"></param>
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));
}
/// <summary>
/// Removes the cache amongst servers for a data type
/// </summary>
/// <param name="dc"></param>
/// <param name="dataType"></param>
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));
}
/// <summary>
/// Refreshes the cache amongst servers for a data type
/// </summary>
/// <param name="dc"></param>
/// <param name="dataType"></param>
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));
}
/// <summary>
/// Removes the cache amongst servers for a data type
/// </summary>
/// <param name="dc"></param>
/// <param name="dataType"></param>
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
/// <summary>
/// Refreshes the cache amongst servers for all pages
/// </summary>
/// <param name="dc"></param>
public static void RefreshAllPageCache(this DistributedCache dc)
{
dc.RefreshAll(new Guid(DistributedCache.PageCacheRefresherId));
dc.RefreshAll(DistributedCache.PageCacheRefresherGuid);
}
/// <summary>
/// Refreshes the cache amongst servers for a page
/// </summary>
/// <param name="dc"></param>
/// <param name="documentId"></param>
public static void RefreshPageCache(this DistributedCache dc, int documentId)
{
dc.Refresh(new Guid(DistributedCache.PageCacheRefresherId), documentId);
dc.Refresh(DistributedCache.PageCacheRefresherGuid, documentId);
}
/// <summary>
/// Refreshes page cache for all instances passed in
/// </summary>
/// <param name="dc"></param>
/// <param name="content"></param>
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);
}
/// <summary>
/// Removes the cache amongst servers for a page
/// </summary>
/// <param name="dc"></param>
/// <param name="content"></param>
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);
}
/// <summary>
/// Removes the cache amongst servers for a page
/// </summary>
/// <param name="dc"></param>
/// <param name="documentId"></param>
public static void RemovePageCache(this DistributedCache dc, int documentId)
{
dc.Remove(new Guid(DistributedCache.PageCacheRefresherId), documentId);
dc.Remove(DistributedCache.PageCacheRefresherGuid, documentId);
}
/// <summary>
/// invokes the unpublished page cache refresher
/// </summary>
/// <param name="dc"></param>
/// <param name="content"></param>
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);
}
/// <summary>
/// invokes the unpublished page cache refresher
/// </summary>
/// <param name="dc"></param>
/// <param name="content"></param>
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);
}
/// <summary>
/// invokes the unpublished page cache refresher to mark all ids for permanent removal
/// </summary>
/// <param name="dc"></param>
/// <param name="contentIds"></param>
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
/// <summary>
/// Refreshes the cache among servers for a member
/// </summary>
/// <param name="dc"></param>
/// <param name="members"></param>
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);
}
/// <summary>
/// Removes the cache among servers for a member
/// </summary>
/// <param name="dc"></param>
/// <param name="members"></param>
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);
}
/// <summary>
/// Refreshes the cache among servers for a member
/// </summary>
/// <param name="dc"></param>
/// <param name="memberId"></param>
[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);
}
/// <summary>
/// Removes the cache among servers for a member
/// </summary>
/// <param name="dc"></param>
/// <param name="memberId"></param>
[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
/// <summary>
/// Refreshes the cache among servers for a member group
/// </summary>
/// <param name="dc"></param>
/// <param name="memberGroupId"></param>
public static void RefreshMemberGroupCache(this DistributedCache dc, int memberGroupId)
{
dc.Refresh(new Guid(DistributedCache.MemberGroupCacheRefresherId), memberGroupId);
dc.Refresh(DistributedCache.MemberGroupCacheRefresherGuid, memberGroupId);
}
/// <summary>
/// Removes the cache among servers for a member group
/// </summary>
/// <param name="dc"></param>
/// <param name="memberGroupId"></param>
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
/// <summary>
/// Refreshes the cache amongst servers for media items
/// </summary>
/// <param name="dc"></param>
/// <param name="media"></param>
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));
}
/// <summary>
/// Refreshes the cache amongst servers for a media item after it's been moved
/// </summary>
/// <param name="dc"></param>
/// <param name="media"></param>
public static void RefreshMediaCacheAfterMoving(this DistributedCache dc, params MoveEventInfo<IMedia>[] media)
{
dc.RefreshByJson(new Guid(DistributedCache.MediaCacheRefresherId),
MediaCacheRefresher.SerializeToJsonPayloadForMoving(
MediaCacheRefresher.OperationType.Saved, media));
dc.RefreshByJson(DistributedCache.MediaCacheRefresherGuid, MediaCacheRefresher.SerializeToJsonPayloadForMoving(MediaCacheRefresher.OperationType.Saved, media));
}
/// <summary>
/// Removes the cache amongst servers for a media item
/// </summary>
/// <param name="dc"></param>
/// <param name="mediaId"></param>
/// <remarks>
/// 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.
/// </remarks>
// 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);
}
/// <summary>
/// Removes the cache among servers for media items when they are recycled
/// </summary>
/// <param name="dc"></param>
/// <param name="media"></param>
public static void RemoveMediaCacheAfterRecycling(this DistributedCache dc, params MoveEventInfo<IMedia>[] media)
{
dc.RefreshByJson(new Guid(DistributedCache.MediaCacheRefresherId),
MediaCacheRefresher.SerializeToJsonPayloadForMoving(
MediaCacheRefresher.OperationType.Trashed, media));
dc.RefreshByJson(DistributedCache.MediaCacheRefresherGuid, MediaCacheRefresher.SerializeToJsonPayloadForMoving(MediaCacheRefresher.OperationType.Trashed, media));
}
/// <summary>
/// Removes the cache among servers for media items when they are permanently deleted
/// </summary>
/// <param name="dc"></param>
/// <param name="mediaIds"></param>
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
/// <summary>
/// Clears the cache for all macros on the current server
/// </summary>
/// <param name="dc"></param>
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);
}
/// <summary>
/// Refreshes the cache amongst servers for a macro item
/// </summary>
/// <param name="dc"></param>
/// <param name="macro"></param>
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));
}
/// <summary>
/// Removes the cache amongst servers for a macro item
/// </summary>
/// <param name="dc"></param>
/// <param name="macro"></param>
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));
}
/// <summary>
/// Refreshes the cache amongst servers for a macro item
/// </summary>
/// <param name="dc"></param>
/// <param name="macro"></param>
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));
}
/// <summary>
/// Removes the cache amongst servers for a macro item
/// </summary>
/// <param name="dc"></param>
/// <param name="macro"></param>
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));
}
/// <summary>
/// Removes the cache amongst servers for a macro item
/// </summary>
/// <param name="dc"></param>
/// <param name="macro"></param>
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
/// <summary>
/// Remove all cache for a given content type
/// </summary>
/// <param name="dc"></param>
/// <param name="contentType"></param>
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));
}
/// <summary>
/// Remove all cache for a given content type
/// </summary>
/// <param name="dc"></param>
/// <param name="contentType"></param>
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
/// <summary>
/// Remove all cache for a given media type
/// </summary>
/// <param name="dc"></param>
/// <param name="mediaType"></param>
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));
}
/// <summary>
/// Remove all cache for a given media type
/// </summary>
/// <param name="dc"></param>
/// <param name="mediaType"></param>
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
/// <summary>
/// Remove all cache for a given media type
/// </summary>
/// <param name="dc"></param>
/// <param name="memberType"></param>
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));
}
/// <summary>
/// Remove all cache for a given media type
/// </summary>
/// <param name="dc"></param>
/// <param name="memberType"></param>
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
}
}
@@ -1,14 +1,44 @@
namespace Umbraco.Web.Routing
{
/// <summary>
/// Reasons a request was not routable on the front-end
/// Represents the outcome of trying to route an incoming request.
/// </summary>
internal enum EnsureRoutableOutcome
{
/// <summary>
/// Request routes to a document.
/// </summary>
/// <remarks>
/// <para>Umbraco was ready and configured, and has content.</para>
/// <para>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.</para>
/// </remarks>
IsRoutable = 0,
/// <summary>
/// Request does not route to a document.
/// </summary>
/// <remarks>
/// <para>Umbraco was ready and configured, and has content.</para>
/// <para>The request does not look like it can be a route to a document. Can be
/// anything else eg back-office, surface controller...</para>
/// </remarks>
NotDocumentRequest = 10,
/// <summary>
/// Umbraco was not ready.
/// </summary>
NotReady = 11,
/// <summary>
/// Umbraco was not configured.
/// </summary>
NotConfigured = 12,
/// <summary>
/// There was no content at all.
/// </summary>
NoContent = 13
}
}
@@ -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
{
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// 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.
/// <para>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.</para>
/// <para>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.</para>
/// </remarks>
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<ServerRegistrationService>(() => applicationContext.Services.ServerRegistrationService)));
//}
/// <summary>
/// Update the database with this entry and bind to request events
/// </summary>
/// <param name="umbracoApplication"></param>
/// <param name="applicationContext"></param>
// 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<ServerRegistrationEventHandler>("Failed to update server record in database.", e);
LogHelper.Error<ServerRegistrationEventHandler>("Failed to update server record in database.", ex);
}
}
//private static IEnumerable<KeyValuePair<string, string>> 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<string, string>(protocol, port);
// }
// }
// }
// }
// }
//}
}
}
+5 -1
View File
@@ -172,6 +172,8 @@
</Reference>
<Reference Include="System.Net.Http.Formatting, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.AspNet.WebApi.Client.4.0.30506.0\lib\net40\System.Net.Http.Formatting.dll</HintPath>
<SpecificVersion>False</SpecificVersion>
<Private>True</Private>
</Reference>
<Reference Include="System.Net.Http.Primitives">
<HintPath>..\packages\Microsoft.Net.Http.2.2.28\lib\net45\System.Net.Http.Primitives.dll</HintPath>
@@ -277,6 +279,7 @@
<Compile Include="Models\ContentEditing\Relation.cs" />
<Compile Include="HtmlStringUtilities.cs" />
<Compile Include="Models\ContentEditing\RelationType.cs" />
<Compile Include="Models\ContentExtensions.cs" />
<Compile Include="IDynamicPublishedContentQuery.cs" />
<Compile Include="ITagQuery.cs" />
<Compile Include="ITypedPublishedContentQuery.cs" />
@@ -289,7 +292,8 @@
<Compile Include="Mvc\MvcVersionCheck.cs" />
<Compile Include="Mvc\ReflectedFixedRazorViewEngine.cs" />
<Compile Include="Scheduling\BackgroundTaskRunner.cs" />
<Compile Include="BatchedServerMessenger.cs" />
<Compile Include="BatchedDatabaseServerMessenger.cs" />
<Compile Include="BatchedWebServiceServerMessenger.cs" />
<Compile Include="CacheHelperExtensions.cs" />
<Compile Include="Cache\ApplicationCacheRefresher.cs" />
<Compile Include="Cache\ApplicationTreeCacheRefresher.cs" />
+10 -3
View File
@@ -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<IIndexer> _indexesToRebuild = new List<IIndexer>();
private readonly List<IIndexer> _indexesToRebuild = new List<IIndexer>();
public WebBootManager(UmbracoApplicationBase umbracoApplication)
: this(umbracoApplication, false)
@@ -324,13 +326,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);
@@ -1,99 +1,105 @@
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>
/// Summary description for CacheRefresher.
/// CacheRefresher web service.
/// </summary>
[WebService(Namespace="http://umbraco.org/webservices/")]
public class CacheRefresher : WebService
{
{
#region Helpers
/// <summary>
/// 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.
/// </summary>
/// <param name="hash"></param>
/// <param name="appDomainAppId"></param>
/// <param name="machineName"></param>
/// <returns></returns>
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 (string.IsNullOrEmpty(hash)) return false; // no hash = don't know = not self
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<CacheRefresher>(
"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<CacheRefresher>(
"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<int[]>(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<int[]>(instruction.JsonIds))
// refresher.Remove(id);
// break;
}
}
}
@@ -101,139 +107,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);
}
/// <summary>
/// Refreshes objects for all Ids matched in the json string
/// </summary>
/// <param name="uniqueIdentifier"></param>
/// <param name="jsonIds">A JSON Serialized string of ids to match</param>
/// <param name="Login"></param>
/// <param name="Password"></param>
[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<int[]>(jsonIds))
refresher.Refresh(id);
}
private void RefreshByIds(Guid uniqueIdentifier, string jsonIds)
{
var serializer = new JavaScriptSerializer();
var ids = serializer.Deserialize<int[]>(jsonIds);
var cr = CacheRefreshersResolver.Current.GetById(uniqueIdentifier);
foreach (var i in ids)
{
cr.Refresh(i);
}
}
/// <summary>
/// Refreshes objects using the passed in Json payload, it will be up to the cache refreshers to deserialize
/// </summary>
/// <param name="uniqueIdentifier"></param>
/// <param name="jsonPayload">A custom JSON payload used by the cache refresher</param>
/// <param name="Login"></param>
/// <param name="Password"></param>
/// <remarks>
/// NOTE: the cache refresher defined by the ID MUST be of type IJsonCacheRefresher or an exception will be thrown
/// </remarks>
[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("<cacheRefreshers/>");
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("<cacheRefreshers/>");
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;
}
}
}
@@ -15,6 +15,8 @@ namespace umbraco.interfaces
void Refresh(int Id);
void Remove(int Id);
void Refresh(Guid Id);
//void Notify(object payload);
}
}