From 7a7528fa53d893107001f1dc2a0f158145d14bbf Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Wed, 24 Sep 2014 18:16:08 -0700 Subject: [PATCH 01/10] Creating Orchard.MessageBus --- .../Brokers/SqlServer/SqlServerBroker.cs | 126 +++++++++++ .../Brokers/SqlServer/Worker.cs | 200 ++++++++++++++++++ .../Handler/MessageBusHandler.cs | 9 + .../Models/MessageRecord.cs | 21 ++ .../Modules/Orchard.MessageBus/Module.txt | 27 +++ .../Orchard.MessageBus.csproj | 154 ++++++++++++++ .../Properties/AssemblyInfo.cs | 37 ++++ .../Orchard.MessageBus/Scripts/Web.config | 16 ++ .../Services/DefaultMessageBus.cs | 31 +++ .../Services/DistributedShellStarter.cs | 51 +++++ .../Services/DistributedShellTrigger.cs | 24 +++ .../Services/DistributedSignals.cs | 35 +++ .../Services/HostNameProvider.cs | 15 ++ .../Services/IHostNameProvider.cs | 11 + .../Services/IMessageBroker.cs | 12 ++ .../Services/IMessageBus.cs | 13 ++ .../MessageBusNotificationProvider.cs | 25 +++ .../SqlServerBrokerMigrations.cs | 25 +++ .../Orchard.MessageBus/Styles/Web.config | 16 ++ .../Modules/Orchard.MessageBus/Web.config | 41 ++++ src/Orchard.sln | 12 +- 21 files changed, 900 insertions(+), 1 deletion(-) create mode 100644 src/Orchard.Web/Modules/Orchard.MessageBus/Brokers/SqlServer/SqlServerBroker.cs create mode 100644 src/Orchard.Web/Modules/Orchard.MessageBus/Brokers/SqlServer/Worker.cs create mode 100644 src/Orchard.Web/Modules/Orchard.MessageBus/Handler/MessageBusHandler.cs create mode 100644 src/Orchard.Web/Modules/Orchard.MessageBus/Models/MessageRecord.cs create mode 100644 src/Orchard.Web/Modules/Orchard.MessageBus/Module.txt create mode 100644 src/Orchard.Web/Modules/Orchard.MessageBus/Orchard.MessageBus.csproj create mode 100644 src/Orchard.Web/Modules/Orchard.MessageBus/Properties/AssemblyInfo.cs create mode 100644 src/Orchard.Web/Modules/Orchard.MessageBus/Scripts/Web.config create mode 100644 src/Orchard.Web/Modules/Orchard.MessageBus/Services/DefaultMessageBus.cs create mode 100644 src/Orchard.Web/Modules/Orchard.MessageBus/Services/DistributedShellStarter.cs create mode 100644 src/Orchard.Web/Modules/Orchard.MessageBus/Services/DistributedShellTrigger.cs create mode 100644 src/Orchard.Web/Modules/Orchard.MessageBus/Services/DistributedSignals.cs create mode 100644 src/Orchard.Web/Modules/Orchard.MessageBus/Services/HostNameProvider.cs create mode 100644 src/Orchard.Web/Modules/Orchard.MessageBus/Services/IHostNameProvider.cs create mode 100644 src/Orchard.Web/Modules/Orchard.MessageBus/Services/IMessageBroker.cs create mode 100644 src/Orchard.Web/Modules/Orchard.MessageBus/Services/IMessageBus.cs create mode 100644 src/Orchard.Web/Modules/Orchard.MessageBus/Services/MessageBusNotificationProvider.cs create mode 100644 src/Orchard.Web/Modules/Orchard.MessageBus/SqlServerBrokerMigrations.cs create mode 100644 src/Orchard.Web/Modules/Orchard.MessageBus/Styles/Web.config create mode 100644 src/Orchard.Web/Modules/Orchard.MessageBus/Web.config diff --git a/src/Orchard.Web/Modules/Orchard.MessageBus/Brokers/SqlServer/SqlServerBroker.cs b/src/Orchard.Web/Modules/Orchard.MessageBus/Brokers/SqlServer/SqlServerBroker.cs new file mode 100644 index 000000000..6c4b00950 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MessageBus/Brokers/SqlServer/SqlServerBroker.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.Data.SqlClient; +using System.Linq; +using System.Security.Permissions; +using System.Threading.Tasks; +using System.Web; +using Orchard.Data; +using Orchard.Environment; +using Orchard.Environment.Configuration; +using Orchard.Environment.Extensions; +using Orchard.Logging; +using Orchard.MessageBus.Models; +using Orchard.MessageBus.Services; +using Orchard.Services; + +namespace Orchard.MessageBus.Brokers.SqlServer { + /// + /// A single connection is maintained, and each subscription will be triggered based on the channel it's listening to + /// + [OrchardFeature("Orchard.MessageBus.SqlServerServiceBroker")] + public class SqlServerBroker : IMessageBroker, IDisposable { + + private IWorker _worker; + private bool _initialized; + private object _synLock = new object(); + + private readonly Work> _messageRecordRepository; + private readonly Work _clock; + private readonly Func _workerFactory; + private readonly ShellSettings _shellSettings; + private readonly Work _hostNameProvider; + + public SqlServerBroker( + Work> messageRecordRepository, + Work clock, + Work hostNameProvider, + Func workerFactory, + ShellSettings shellSettings + ) { + _messageRecordRepository = messageRecordRepository; + _clock = clock; + _shellSettings = shellSettings; + _workerFactory = workerFactory; + _hostNameProvider = hostNameProvider; + + Logger = NullLogger.Instance; + } + + public ILogger Logger { get; set; } + + public bool EnsureInitialized() { + lock (_synLock) { + if (!_initialized) { + try { + // call only once per connectionstring when appdomain starts up + Logger.Information("Starting SqlDependency."); + SqlDependency.Start(_shellSettings.DataConnectionString); + + _worker = _workerFactory(); + _worker.Work(); + + _initialized = true; + } + catch (Exception e) { + Logger.Error("The application doesn't have the permission to request notifications.", e); + } + } + + return _initialized; + } + } + + public void Subscribe(string channel, Action handler) { + if (!EnsureInitialized()) { + return; + } + + try { + lock (_synLock) { + _worker.RegisterHandler(channel, handler); + } + } + catch(Exception e) { + Logger.Error("An error occured while creating a Worker.", e); + } + } + + public void Publish(string channel, string message) { + if (!EnsureInitialized()) { + return; + } + + // clear old messages on publish to get a single worker + var oldMessages = _messageRecordRepository.Value + .Table + .Where(x => x.CreatedUtc <= _clock.Value.UtcNow.AddHours(-1)) + .ToList(); + + foreach (var messageRecord in oldMessages) { + _messageRecordRepository.Value.Delete(messageRecord); + } + + _messageRecordRepository.Value.Create( + new MessageRecord { + Channel = channel, + Message = message, + Publisher = _hostNameProvider.Value.GetHostName(), + CreatedUtc = _clock.Value.UtcNow + } + ); + } + + public void Dispose() { + // call only once per connectionstring when appdomain shuts down + if (!String.IsNullOrWhiteSpace(_shellSettings.DataConnectionString)) { + SqlDependency.Stop(_shellSettings.DataConnectionString); + } + } + + private string GetHostName() { + // use the current host and the process id as two servers could run on the same machine + return System.Net.Dns.GetHostName() + ":" + System.Diagnostics.Process.GetCurrentProcess().Id; + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.MessageBus/Brokers/SqlServer/Worker.cs b/src/Orchard.Web/Modules/Orchard.MessageBus/Brokers/SqlServer/Worker.cs new file mode 100644 index 000000000..4bac8d4ba --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MessageBus/Brokers/SqlServer/Worker.cs @@ -0,0 +1,200 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.SqlClient; +using System.Linq; +using System.Threading; +using System.Web; +using System.Web.Hosting; +using Orchard.Environment.Configuration; +using Orchard.Environment.Extensions; +using Orchard.Logging; +using Orchard.MessageBus.Models; +using Orchard.MessageBus.Services; + +namespace Orchard.MessageBus.Brokers.SqlServer { + public interface IWorker : IDependency { + void Work(); + void RegisterHandler(string channel, Action handler); + } + + [OrchardFeature("Orchard.MessageBus.SqlServerServiceBroker")] + public class Worker : IWorker, IRegisteredObject { + + private readonly ShellSettings _shellSettings; + private readonly IHostNameProvider _hostNameProvider; + + private SqlDependency _dependency; + + private static string commandText = "SELECT Id, Channel, Publisher, Message, CreatedUtc FROM dbo.{0}Orchard_MessageBus_MessageRecord WHERE Id > @Id"; + private static int lastMessageId = 0; + private bool _stopped; + + private Dictionary>> _handlers = new Dictionary>>(); + + public Worker(ShellSettings shellSettings, IHostNameProvider hostNameProvider) { + _hostNameProvider = hostNameProvider; + _shellSettings = shellSettings; + + var tablePrefix = _shellSettings.DataTablePrefix; + if (!String.IsNullOrWhiteSpace(tablePrefix)) { + tablePrefix += "_"; + } + + commandText = String.Format(commandText, tablePrefix); + + Logger = NullLogger.Instance; + } + + public ILogger Logger { get; set; } + + public void Work() { + // exit loop if stop notification as been triggered + if (_stopped) { + return; + } + + try { + IEnumerable messages; + + // load and process existing messages + using (var connection = new SqlConnection(_shellSettings.DataConnectionString)) { + connection.Open(); + + var command = CreateCommand(connection); + messages = GetMessages(command); + } + + ProcessMessages(messages); + + // wait for new messages to be available + WaitForWork(); + + } + catch (Exception e) { + Logger.Error("An unexpected error occured while monitoring sql dependencies.", e); + } + } + + private void DoWork(object sender, SqlNotificationEventArgs eventArgs) { + Work(); + } + + private void WaitForWork() { + + using (var connection = new SqlConnection(_shellSettings.DataConnectionString)) { + connection.Open(); + + using (var command = CreateCommand(connection)) { + + // create a sql depdendency on the table we are monitoring + _dependency = new SqlDependency(command); + + // when new records are present, continue the thread + _dependency.OnChange += DoWork; + + // start monitoring the table + command.ExecuteNonQuery(); + } + } + } + + private void ProcessMessages(IEnumerable messages) { + + // if this is the first time it's executed we just need to get the highest Id + if (lastMessageId == 0) { + lastMessageId = messages.Max(x => x.Id); + return; + } + + // process the messages synchronously and in order of publication + foreach (var message in messages.OrderBy(x => x.Id)) { + + // save the latest message id so that next time the table is monitored + // we get notified for new messages + lastMessageId = message.Id; + + // only process handlers registered for the specific channel + List> channelHandlers; + if (_handlers.TryGetValue(message.Channel, out channelHandlers)) { + + var hostName = _hostNameProvider.GetHostName(); + + // execute subscription + foreach (var handler in channelHandlers) { + + // ignore messages sent by the current host + if (!message.Publisher.Equals(hostName, StringComparison.OrdinalIgnoreCase)) { + handler(message.Channel, message.Message); + } + + // stop processing other messages if stop has been required + if (_stopped) { + return; + } + } + } + } + } + + public void Stop(bool immediate) { + _stopped = true; + } + + public void RegisterHandler(string channel, Action handler) { + GetHandlersForChannel(channel).Add(handler); + } + + private List> GetHandlersForChannel(string channel) { + List> channelHandlers; + + if(!_handlers.TryGetValue(channel, out channelHandlers)) { + channelHandlers = new List>(); + _handlers.Add(channel, channelHandlers); + } + + return channelHandlers; + } + + public SqlCommand CreateCommand(SqlConnection connection) { + SqlCommand command = new SqlCommand(commandText, connection); + + SqlParameter param = new SqlParameter("@Id", SqlDbType.Int); + param.Direction = ParameterDirection.Input; + param.DbType = DbType.Int32; + param.Value = lastMessageId; + command.Parameters.Add(param); + + return command; + } + + public IEnumerable GetMessages(SqlCommand command) { + var result = new List(); + + try { + + using (var reader = command.ExecuteReader()) { + if (reader.HasRows) { + while (reader.Read()) { + result.Add(new MessageRecord { + Id = reader.GetInt32(0), + Channel = reader.GetString(1), + Publisher = reader.GetString(2), + Message = reader.GetString(3), + CreatedUtc = reader.GetDateTime(4) + }); + } + } + } + } + catch (Exception e) { + Logger.Error("Could not retreive Sql Broker messages.", e); + return Enumerable.Empty(); + } + + return result; + } + + + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.MessageBus/Handler/MessageBusHandler.cs b/src/Orchard.Web/Modules/Orchard.MessageBus/Handler/MessageBusHandler.cs new file mode 100644 index 000000000..296350c8d --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MessageBus/Handler/MessageBusHandler.cs @@ -0,0 +1,9 @@ +using System.Linq; +using JetBrains.Annotations; +using Orchard.Data; +using Orchard.ContentManagement.Handlers; + +namespace Orchard.MessageBus.Handlers { + public class MessageBusHandler : ContentHandler { + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.MessageBus/Models/MessageRecord.cs b/src/Orchard.Web/Modules/Orchard.MessageBus/Models/MessageRecord.cs new file mode 100644 index 000000000..bd6924662 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MessageBus/Models/MessageRecord.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using Orchard.Data.Conventions; +using Orchard.Environment.Extensions; + +namespace Orchard.MessageBus.Models { + + [OrchardFeature("Orchard.MessageBus.SqlServerServiceBroker")] + public class MessageRecord { + + public virtual int Id { get; set; } + public virtual string Publisher { get; set; } + public virtual string Channel { get; set; } + public virtual DateTime CreatedUtc { get; set; } + + [StringLengthMax] + public virtual string Message { get; set; } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.MessageBus/Module.txt b/src/Orchard.Web/Modules/Orchard.MessageBus/Module.txt new file mode 100644 index 000000000..96dfd8582 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MessageBus/Module.txt @@ -0,0 +1,27 @@ +Name: Orchard.MessageBus +AntiForgery: enabled +Author: The Orchard Team +Website: http://orchardproject.net +Version: 1.0 +OrchardVersion: 1.0 +Description: Provides communication APIs for server farms. +Features: + Orchard.MessageBus: + Name: Message Bus + Description: Reusable API abstractions to communicate in a server farm. + Category: Hosting + Orchard.MessageBus.DistributedSignals: + Name: Distributed Signals + Description: Distribute signals cache invalidation calls. + Dependencies: Orchard.MessageBus + Category: Hosting + Orchard.MessageBus.SqlServerServiceBroker: + Name: SQL Server Service Broker + Description: A message bus implementation using SQL Server Service Broker. + Dependencies: Orchard.MessageBus + Category: Hosting + Orchard.MessageBus.DistributedShellRestart + Name: Distributed Shell Restart + Description: Distribute shell restarts. + Dependencies: Orchard.MessageBus + Category: Hosting diff --git a/src/Orchard.Web/Modules/Orchard.MessageBus/Orchard.MessageBus.csproj b/src/Orchard.Web/Modules/Orchard.MessageBus/Orchard.MessageBus.csproj new file mode 100644 index 000000000..600bf4d12 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MessageBus/Orchard.MessageBus.csproj @@ -0,0 +1,154 @@ + + + + + Debug + AnyCPU + 9.0.30729 + 2.0 + {ED715544-E649-4F48-B8EE-9368C41C3AC0} + {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} + Library + Properties + Orchard.MessageBus + Orchard.MessageBus + v4.5.1 + false + + + 4.0 + + + false + + + + + + + true + full + false + bin\ + DEBUG;TRACE + prompt + 4 + AllRules.ruleset + false + + + pdbonly + true + bin\ + TRACE + prompt + 4 + AllRules.ruleset + false + + + + + False + ..\..\..\..\lib\nhibernate\NHibernate.dll + + + + + 3.5 + + + + False + ..\..\..\..\lib\aspnetmvc\System.Web.Mvc.dll + + + + + + + + + + + + + + + + + + + {2D1D92BB-4555-4CBE-8D0E-63563D6CE4C6} + Orchard.Framework + + + {9916839C-39FC-4CEB-A5AF-89CA7E87119F} + Orchard.Core + + + + + + + + + + + + + + + + + + + + + + + + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + + + + $(ProjectDir)\..\Manifests + + + + + + + + + + + + False + True + 45979 + / + + + False + True + http://orchard.codeplex.com + False + + + + + \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.MessageBus/Properties/AssemblyInfo.cs b/src/Orchard.Web/Modules/Orchard.MessageBus/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..220f9c927 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MessageBus/Properties/AssemblyInfo.cs @@ -0,0 +1,37 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Security; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Orchard.MessageBus")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyProduct("Orchard")] +[assembly: AssemblyCopyright("")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("5b79fe08-1e5b-44ab-9323-2c08a7e461e5")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Revision and Build Numbers +// by using the '*' as shown below: + +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] + diff --git a/src/Orchard.Web/Modules/Orchard.MessageBus/Scripts/Web.config b/src/Orchard.Web/Modules/Orchard.MessageBus/Scripts/Web.config new file mode 100644 index 000000000..817198995 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MessageBus/Scripts/Web.config @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/src/Orchard.Web/Modules/Orchard.MessageBus/Services/DefaultMessageBus.cs b/src/Orchard.Web/Modules/Orchard.MessageBus/Services/DefaultMessageBus.cs new file mode 100644 index 000000000..876b87d0a --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MessageBus/Services/DefaultMessageBus.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using System.Threading.Tasks; + +namespace Orchard.MessageBus.Services { + public class DefaultMessageBus : IMessageBus { + private readonly IMessageBroker _messageBroker; + + public DefaultMessageBus(IEnumerable messageBrokers) { + _messageBroker = messageBrokers.FirstOrDefault(); + } + + public void Subscribe(string channel, Action handler) { + if (_messageBroker == null) { + return; + } + + _messageBroker.Subscribe(channel, handler); + } + + public void Publish(string channel, string message) { + if (_messageBroker == null) { + return; + } + + _messageBroker.Publish(channel, message); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.MessageBus/Services/DistributedShellStarter.cs b/src/Orchard.Web/Modules/Orchard.MessageBus/Services/DistributedShellStarter.cs new file mode 100644 index 000000000..97bef5f20 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MessageBus/Services/DistributedShellStarter.cs @@ -0,0 +1,51 @@ +using System.Linq; +using Orchard.Caching; +using Orchard.Environment; +using Orchard.Environment.Configuration; +using Orchard.Environment.Extensions; + +namespace Orchard.MessageBus.Services { + + public interface IDistributedShellStarter : ISingletonDependency { + + } + + [OrchardFeature("Orchard.MessageBus.DistributedShellRestart")] + public class DistributedShellStarter : IDistributedShellStarter, IOrchardShellEvents { + private readonly IWorkContextAccessor _workContextAccessor; + + private readonly IMessageBus _messageBus; + + public readonly static string Channel = "ShellChanged"; + + public DistributedShellStarter(IMessageBus messageBus, IWorkContextAccessor workContextAccessor) { + _messageBus = messageBus; + _workContextAccessor = workContextAccessor; + } + + public void Activated() { + _messageBus.Subscribe(Channel, (channel, message) => { + // todo: this only handles changed tenants, we should consider handling started and stopped tenants + + using (var scope = _workContextAccessor.CreateWorkContextScope()) { + var shellSettings = scope.Resolve(); + if (shellSettings != null) { + + // todo: this doesn't work as the new tenants list is lost right after + var shellSettingsManagerEventHandler = scope.Resolve(); + shellSettingsManagerEventHandler.Saved(shellSettings); + + var orchardHost = scope.Resolve() as DefaultOrchardHost; + if(orchardHost != null) { + var startUpdatedShellsMethod = typeof(DefaultOrchardHost).GetMethod("StartUpdatedShells", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + startUpdatedShellsMethod.Invoke(orchardHost, null); + } + } + } + }); + } + + public void Terminating() { + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.MessageBus/Services/DistributedShellTrigger.cs b/src/Orchard.Web/Modules/Orchard.MessageBus/Services/DistributedShellTrigger.cs new file mode 100644 index 000000000..d5e888fe0 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MessageBus/Services/DistributedShellTrigger.cs @@ -0,0 +1,24 @@ +using Orchard.Environment.Configuration; +using Orchard.Environment.Descriptor; +using Orchard.Environment.Descriptor.Models; +using Orchard.Environment.Extensions; + +namespace Orchard.MessageBus.Services { + [OrchardFeature("Orchard.MessageBus.DistributedShellRestart")] + public class DistributedShellTrigger : IShellDescriptorManagerEventHandler, IShellSettingsManagerEventHandler { + + private readonly IMessageBus _messageBus; + + public DistributedShellTrigger(IShellSettingsManager shellSettingsManager, IMessageBus messageBus, IShellSettingsManagerEventHandler shellSettingsManagerEventHandler) { + _messageBus = messageBus; + } + + void IShellDescriptorManagerEventHandler.Changed(ShellDescriptor descriptor, string tenant) { + _messageBus.Publish(DistributedShellStarter.Channel, tenant); + } + + void IShellSettingsManagerEventHandler.Saved(ShellSettings settings) { + _messageBus.Publish(DistributedShellStarter.Channel, settings.Name); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.MessageBus/Services/DistributedSignals.cs b/src/Orchard.Web/Modules/Orchard.MessageBus/Services/DistributedSignals.cs new file mode 100644 index 000000000..c24661d5a --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MessageBus/Services/DistributedSignals.cs @@ -0,0 +1,35 @@ +using Orchard.Caching; +using Orchard.Environment; +using Orchard.Environment.Extensions; +using Orchard.MessageBus.Services; +using Orchard.Services; + +namespace Orchard.MessageBus.Services { + [OrchardFeature("Orchard.MessageBus.DistributedSignals")] + [OrchardSuppressDependency("Orchard.Caching.Signals")] + public class DistributedSignals : Signals, ISignals, IOrchardShellEvents { + private readonly IMessageBus _messageBus; + + public DistributedSignals(IMessageBus messageBus) { + _messageBus = messageBus; + } + + void ISignals.Trigger(T signal) { + base.Trigger(signal); + _messageBus.Publish("Signal", signal.ToString()); + } + + IVolatileToken ISignals.When(T signal) { + return base.When(signal); + } + + public void Activated() { + _messageBus.Subscribe("Signal", (channel, message) => { + base.Trigger(message); + }); + } + + public void Terminating() { + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.MessageBus/Services/HostNameProvider.cs b/src/Orchard.Web/Modules/Orchard.MessageBus/Services/HostNameProvider.cs new file mode 100644 index 000000000..af4aeb48c --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MessageBus/Services/HostNameProvider.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Orchard.MessageBus.Services { + public class HostNameProvider : IHostNameProvider { + + public string GetHostName() { + // use the current host and the process id as two servers could run on the same machine + return System.Net.Dns.GetHostName() + ":" + System.Diagnostics.Process.GetCurrentProcess().Id; + } + } +} diff --git a/src/Orchard.Web/Modules/Orchard.MessageBus/Services/IHostNameProvider.cs b/src/Orchard.Web/Modules/Orchard.MessageBus/Services/IHostNameProvider.cs new file mode 100644 index 000000000..5e1583c0b --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MessageBus/Services/IHostNameProvider.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Orchard.MessageBus.Services { + public interface IHostNameProvider : IDependency { + string GetHostName(); + } +} diff --git a/src/Orchard.Web/Modules/Orchard.MessageBus/Services/IMessageBroker.cs b/src/Orchard.Web/Modules/Orchard.MessageBus/Services/IMessageBroker.cs new file mode 100644 index 000000000..d7d5a1711 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MessageBus/Services/IMessageBroker.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Orchard.MessageBus.Services { + public interface IMessageBroker : ISingletonDependency { + void Subscribe(string channel, Action handler); + void Publish(string channel, string message); + } +} diff --git a/src/Orchard.Web/Modules/Orchard.MessageBus/Services/IMessageBus.cs b/src/Orchard.Web/Modules/Orchard.MessageBus/Services/IMessageBus.cs new file mode 100644 index 000000000..3e691babf --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MessageBus/Services/IMessageBus.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Orchard.MessageBus.Models; + +namespace Orchard.MessageBus.Services { + public interface IMessageBus : ISingletonDependency { + void Subscribe(string channel, Action handler); + void Publish(string channel, string message); + } +} diff --git a/src/Orchard.Web/Modules/Orchard.MessageBus/Services/MessageBusNotificationProvider.cs b/src/Orchard.Web/Modules/Orchard.MessageBus/Services/MessageBusNotificationProvider.cs new file mode 100644 index 000000000..715e69a4d --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MessageBus/Services/MessageBusNotificationProvider.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using Orchard.Localization; +using Orchard.UI.Admin.Notification; +using Orchard.UI.Notify; +using System.Linq; + +namespace Orchard.MessageBus.Services { + public class MessageBusNotificationProvider : INotificationProvider { + private readonly IEnumerable _messageBrokers; + + public MessageBusNotificationProvider(IEnumerable messageBrokers) { + _messageBrokers = messageBrokers; + T = NullLocalizer.Instance; + } + + public Localizer T { get; set; } + + public IEnumerable GetNotifications() { + + if (!_messageBrokers.Any()) { + yield return new NotifyEntry { Message = T("You need to enable an message bus broker implementation like SQL Server Service Broker."), Type = NotifyType.Warning }; + } + } + } +} diff --git a/src/Orchard.Web/Modules/Orchard.MessageBus/SqlServerBrokerMigrations.cs b/src/Orchard.Web/Modules/Orchard.MessageBus/SqlServerBrokerMigrations.cs new file mode 100644 index 000000000..cc29857c2 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MessageBus/SqlServerBrokerMigrations.cs @@ -0,0 +1,25 @@ +using System; +using Orchard.ContentManagement.MetaData; +using Orchard.Data.Migration; +using Orchard.Environment.Extensions; +using Orchard.MessageBus.Models; + +namespace Orchard.MessageBus { + [OrchardFeature("Orchard.MessageBus.SqlServerServiceBroker")] + public class SqlServerBrokerMigrations : DataMigrationImpl { + + public int Create() { + + SchemaBuilder.CreateTable("MessageRecord", + table => table + .Column("Id", c => c.PrimaryKey().Identity()) + .Column("Publisher", c => c.WithLength(255)) + .Column("Channel", c => c.WithLength(255)) + .Column("Message", c => c.Unlimited()) + .Column("CreatedUtc") + ); + + return 1; + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.MessageBus/Styles/Web.config b/src/Orchard.Web/Modules/Orchard.MessageBus/Styles/Web.config new file mode 100644 index 000000000..817198995 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MessageBus/Styles/Web.config @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/src/Orchard.Web/Modules/Orchard.MessageBus/Web.config b/src/Orchard.Web/Modules/Orchard.MessageBus/Web.config new file mode 100644 index 000000000..6fc84bc30 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MessageBus/Web.config @@ -0,0 +1,41 @@ + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Orchard.sln b/src/Orchard.sln index 1a9be3e7f..216369f8b 100644 --- a/src/Orchard.sln +++ b/src/Orchard.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 2013 -VisualStudioVersion = 12.0.30501.0 +VisualStudioVersion = 12.0.30723.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Modules", "Modules", "{E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5}" EndProject @@ -243,6 +243,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.Azure.MediaServices EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.AuditTrail", "Orchard.Web\Modules\Orchard.AuditTrail\Orchard.AuditTrail.csproj", "{3DD574CD-9C5D-4A45-85E1-EBBA64C22B5F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.MessageBus", "Orchard.Web\Modules\Orchard.MessageBus\Orchard.MessageBus.csproj", "{ED715544-E649-4F48-B8EE-9368C41C3AC0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution CodeCoverage|Any CPU = CodeCoverage|Any CPU @@ -1008,6 +1010,13 @@ Global {3DD574CD-9C5D-4A45-85E1-EBBA64C22B5F}.FxCop|Any CPU.ActiveCfg = Release|Any CPU {3DD574CD-9C5D-4A45-85E1-EBBA64C22B5F}.Release|Any CPU.ActiveCfg = Release|Any CPU {3DD574CD-9C5D-4A45-85E1-EBBA64C22B5F}.Release|Any CPU.Build.0 = Release|Any CPU + {ED715544-E649-4F48-B8EE-9368C41C3AC0}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU + {ED715544-E649-4F48-B8EE-9368C41C3AC0}.Coverage|Any CPU.ActiveCfg = Release|Any CPU + {ED715544-E649-4F48-B8EE-9368C41C3AC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ED715544-E649-4F48-B8EE-9368C41C3AC0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ED715544-E649-4F48-B8EE-9368C41C3AC0}.FxCop|Any CPU.ActiveCfg = Release|Any CPU + {ED715544-E649-4F48-B8EE-9368C41C3AC0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ED715544-E649-4F48-B8EE-9368C41C3AC0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1087,5 +1096,6 @@ Global {7528BF74-25C7-4ABE-883A-443B4EEC4776} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5} {14A96B1A-9DC9-44C8-A675-206329E15263} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5} {3DD574CD-9C5D-4A45-85E1-EBBA64C22B5F} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5} + {ED715544-E649-4F48-B8EE-9368C41C3AC0} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5} EndGlobalSection EndGlobal From d0c7091964b7a76734fdfd4320a0a5c80d485731 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Tue, 30 Sep 2014 16:25:49 -0700 Subject: [PATCH 02/10] Adding Orchard.Redis module --- .../Modules/Orchard.Azure/Module.txt | 5 - .../Orchard.Azure/Orchard.Azure.csproj | 4 - .../AzureRedisOutputCacheStorageProvider.cs | 166 ------------------ .../Brokers/SqlServer/Worker.cs | 16 +- .../Services/DefaultMessageBus.cs | 12 ++ .../Modules/Orchard.OutputCache/Module.txt | 2 +- .../Configuration/IRedisConnectionProvider.cs | 10 ++ .../Configuration/RedisConnectionProvider.cs | 47 +++++ .../MessageBus/RedisMessageBusBroker.cs | 80 +++++++++ .../Modules/Orchard.Redis/Module.txt | 22 +++ .../Orchard.Redis/Orchard.Redis.csproj | 153 ++++++++++++++++ .../RedisOutputCacheStorageProvider.cs | 113 ++++++++++++ .../Orchard.Redis/Properties/AssemblyInfo.cs | 37 ++++ .../Modules/Orchard.Redis/Web.config | 41 +++++ src/Orchard.sln | 13 ++ 15 files changed, 537 insertions(+), 184 deletions(-) delete mode 100644 src/Orchard.Web/Modules/Orchard.Azure/Services/Caching/Output/AzureRedisOutputCacheStorageProvider.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Redis/Configuration/IRedisConnectionProvider.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Redis/Configuration/RedisConnectionProvider.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Redis/MessageBus/RedisMessageBusBroker.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Redis/Module.txt create mode 100644 src/Orchard.Web/Modules/Orchard.Redis/Orchard.Redis.csproj create mode 100644 src/Orchard.Web/Modules/Orchard.Redis/OutputCache/RedisOutputCacheStorageProvider.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Redis/Properties/AssemblyInfo.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Redis/Web.config diff --git a/src/Orchard.Web/Modules/Orchard.Azure/Module.txt b/src/Orchard.Web/Modules/Orchard.Azure/Module.txt index a3c4bd1e0..db10c6055 100644 --- a/src/Orchard.Web/Modules/Orchard.Azure/Module.txt +++ b/src/Orchard.Web/Modules/Orchard.Azure/Module.txt @@ -17,11 +17,6 @@ Features: Description: Activates an Orchard output cache provider that targets Windows Azure Cache. Dependencies: Orchard.Azure, Orchard.OutputCache Category: Performance - Orchard.Azure.RedisOutputCache: - Name: Microsoft Azure Redis Output Cache - Description: Activates an Orchard output cache provider that targets Windows Azure Redis Cache. - Dependencies: Orchard.Azure, Orchard.OutputCache - Category: Performance Orchard.Azure.DatabaseCache: Name: Microsoft Azure Database Cache Description: Activates an NHibernate second-level cache provider that targets Microsoft Azure Cache. diff --git a/src/Orchard.Web/Modules/Orchard.Azure/Orchard.Azure.csproj b/src/Orchard.Web/Modules/Orchard.Azure/Orchard.Azure.csproj index aa6a2c637..a9fe59a56 100644 --- a/src/Orchard.Web/Modules/Orchard.Azure/Orchard.Azure.csproj +++ b/src/Orchard.Web/Modules/Orchard.Azure/Orchard.Azure.csproj @@ -93,9 +93,6 @@ False ..\..\..\..\lib\nhibernate\NHibernate.dll - - ..\..\..\..\lib\redis\StackExchange.Redis.dll - @@ -111,7 +108,6 @@ - diff --git a/src/Orchard.Web/Modules/Orchard.Azure/Services/Caching/Output/AzureRedisOutputCacheStorageProvider.cs b/src/Orchard.Web/Modules/Orchard.Azure/Services/Caching/Output/AzureRedisOutputCacheStorageProvider.cs deleted file mode 100644 index 3b1647d94..000000000 --- a/src/Orchard.Web/Modules/Orchard.Azure/Services/Caching/Output/AzureRedisOutputCacheStorageProvider.cs +++ /dev/null @@ -1,166 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using Microsoft.ApplicationServer.Caching; -using Newtonsoft.Json; -using Orchard.Azure.Services.Environment.Configuration; -using Orchard.Caching; -using Orchard.Environment.Configuration; -using Orchard.Environment.Extensions; -using Orchard.Logging; -using Orchard.OutputCache.Models; -using Orchard.OutputCache.Services; -using StackExchange.Redis; - -namespace Orchard.Azure.Services.Caching.Output { - - [OrchardFeature("Orchard.Azure.RedisOutputCache")] - [OrchardSuppressDependency("Orchard.OutputCache.Services.DefaultCacheStorageProvider")] - public class AzureRedisCacheStorageProvider : Component, IOutputCacheStorageProvider { - public const string DataCacheKey = "DataCache"; - public const string ClientConfigurationKey = "CacheClientConfiguration"; - - private CacheClientConfiguration _cacheClientConfiguration; - private static ConcurrentDictionary _connectionMultiplexers = new ConcurrentDictionary(); - - private readonly ICacheManager _cacheManager; - private readonly ShellSettings _shellSettings; - private readonly IPlatformConfigurationAccessor _pca; - private HashSet _keysCache; - - public AzureRedisCacheStorageProvider( - ShellSettings shellSettings, - IPlatformConfigurationAccessor pca, - ICacheManager cacheManager) { - _cacheManager = cacheManager; - _shellSettings = shellSettings; - _pca = pca; - - Logger = NullLogger.Instance; - } - - public CacheClientConfiguration CacheConfiguration { - get { - // the configuration is backed by a field so that we don't call the cacheManager multiple times in the same request - // cache configurations are stored in the cacheManager so that we don't read the config on each request - if (_cacheClientConfiguration == null) { - - _cacheClientConfiguration = _cacheManager.Get(ClientConfigurationKey, ctx => { - CacheClientConfiguration cacheConfig; - try { - cacheConfig = CacheClientConfiguration.FromPlatformConfiguration( - _shellSettings.Name, - Constants.OutputCacheSettingNamePrefix, - _pca); - - cacheConfig.Validate(); - return cacheConfig; - } - catch (Exception ex) { - throw new Exception(String.Format("The {0} configuration settings are missing or invalid.", Constants.OutputCacheFeatureName), ex); - } - }); - - if (_cacheClientConfiguration == null) { - throw new InvalidOperationException("Could not create a valid cache configuration"); - } - } - - return _cacheClientConfiguration; - } - } - - public IDatabase Cache { - get { - - return GetConnection().GetDatabase(); - } - } - - private ConnectionMultiplexer GetConnection() { - var connectionMultiplexer = _connectionMultiplexers.GetOrAdd(CacheConfiguration, cfg => { - Logger.Debug("Creating a new cache client ({0})", CacheConfiguration.GetHashCode()); - var connectionString = cfg.HostIdentifier + ",password=" + cfg.AuthorizationToken; - return ConnectionMultiplexer.Connect(connectionString); - }); - - - return connectionMultiplexer; - } - - public void Set(string key, CacheItem cacheItem) { - if (cacheItem.ValidFor <= 0) { - return; - } - - var value = JsonConvert.SerializeObject(cacheItem); - Cache.StringSet(GetLocalizedKey(key), value, TimeSpan.FromSeconds(cacheItem.ValidFor)); - } - - public void Remove(string key) { - Cache.KeyDelete(GetLocalizedKey(key)); - } - - public void RemoveAll() { - foreach (var key in GetAllKeys()) { - Remove(key); - } - } - - public CacheItem GetCacheItem(string key) { - string value = Cache.StringGet(GetLocalizedKey(key)); - if(String.IsNullOrEmpty(value)) { - return null; - } - - return JsonConvert.DeserializeObject(value); - } - - public IEnumerable GetCacheItems(int skip, int count) { - foreach (var key in GetAllKeys().Skip(skip).Take(count)) { - var cacheItem = GetCacheItem(key); - // the item could have expired in the meantime - if (cacheItem != null) { - yield return cacheItem; - } - } - } - - public int GetCacheItemsCount() { - return GetAllKeys().Count(); - } - - /// - /// Creates a namespaced key to support multiple tenants on top of a single Redis connection. - /// - /// The key to localized. - /// A localized key based on the tenant name. - private string GetLocalizedKey(string key) { - return _shellSettings.Name + ":" + key; - } - - /// - /// Returns all the keys for the current tenant. - /// - /// The keys for the current tenant. - private IEnumerable GetAllKeys() { - // prevent the same request from computing the list twice (count + list) - if (_keysCache == null) { - _keysCache = new HashSet(); - var prefix = GetLocalizedKey(""); - - var connection = GetConnection(); - foreach (var endPoint in connection.GetEndPoints()) { - var server = GetConnection().GetServer(endPoint); - foreach (var key in server.Keys(pattern: GetLocalizedKey("*"))) { - _keysCache.Add(key.ToString().Substring(prefix.Length)); - } - } - } - - return _keysCache; - } - } -} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.MessageBus/Brokers/SqlServer/Worker.cs b/src/Orchard.Web/Modules/Orchard.MessageBus/Brokers/SqlServer/Worker.cs index 4bac8d4ba..cc6ab616e 100644 --- a/src/Orchard.Web/Modules/Orchard.MessageBus/Brokers/SqlServer/Worker.cs +++ b/src/Orchard.Web/Modules/Orchard.MessageBus/Brokers/SqlServer/Worker.cs @@ -145,16 +145,16 @@ namespace Orchard.MessageBus.Brokers.SqlServer { GetHandlersForChannel(channel).Add(handler); } - private List> GetHandlersForChannel(string channel) { - List> channelHandlers; + private List> GetHandlersForChannel(string channel) { + List> channelHandlers; - if(!_handlers.TryGetValue(channel, out channelHandlers)) { - channelHandlers = new List>(); - _handlers.Add(channel, channelHandlers); - } + if(!_handlers.TryGetValue(channel, out channelHandlers)) { + channelHandlers = new List>(); + _handlers.Add(channel, channelHandlers); + } - return channelHandlers; - } + return channelHandlers; + } public SqlCommand CreateCommand(SqlConnection connection) { SqlCommand command = new SqlCommand(commandText, connection); diff --git a/src/Orchard.Web/Modules/Orchard.MessageBus/Services/DefaultMessageBus.cs b/src/Orchard.Web/Modules/Orchard.MessageBus/Services/DefaultMessageBus.cs index 876b87d0a..e38098db8 100644 --- a/src/Orchard.Web/Modules/Orchard.MessageBus/Services/DefaultMessageBus.cs +++ b/src/Orchard.Web/Modules/Orchard.MessageBus/Services/DefaultMessageBus.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Web; using System.Threading.Tasks; +using Orchard.Logging; namespace Orchard.MessageBus.Services { public class DefaultMessageBus : IMessageBus { @@ -10,14 +11,19 @@ namespace Orchard.MessageBus.Services { public DefaultMessageBus(IEnumerable messageBrokers) { _messageBroker = messageBrokers.FirstOrDefault(); + + Logger = NullLogger.Instance; } + public ILogger Logger { get; set; } + public void Subscribe(string channel, Action handler) { if (_messageBroker == null) { return; } _messageBroker.Subscribe(channel, handler); + Logger.Debug("{0} subscribed to {1}", GetHostName(), channel); } public void Publish(string channel, string message) { @@ -26,6 +32,12 @@ namespace Orchard.MessageBus.Services { } _messageBroker.Publish(channel, message); + Logger.Debug("{0} published {1}", GetHostName(), channel); + } + + private string GetHostName() { + // use the current host and the process id as two servers could run on the same machine + return System.Net.Dns.GetHostName() + ":" + System.Diagnostics.Process.GetCurrentProcess().Id; } } } \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.OutputCache/Module.txt b/src/Orchard.Web/Modules/Orchard.OutputCache/Module.txt index e20077aff..30e696b98 100644 --- a/src/Orchard.Web/Modules/Orchard.OutputCache/Module.txt +++ b/src/Orchard.Web/Modules/Orchard.OutputCache/Module.txt @@ -12,4 +12,4 @@ Features: Orchard.OutputCache.Database: Description: Stores output cache data in the database. Category: Performance - Dependencies: Orchard.OutputCache + Dependencies: Orchard.OutputCache diff --git a/src/Orchard.Web/Modules/Orchard.Redis/Configuration/IRedisConnectionProvider.cs b/src/Orchard.Web/Modules/Orchard.Redis/Configuration/IRedisConnectionProvider.cs new file mode 100644 index 000000000..5caeba2a1 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Redis/Configuration/IRedisConnectionProvider.cs @@ -0,0 +1,10 @@ +using StackExchange.Redis; + +namespace Orchard.Redis.Configuration { + public interface IRedisConnectionProvider : ISingletonDependency { + + ConnectionMultiplexer GetConnection(string connectionString); + + string GetConnectionString(string service); + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Redis/Configuration/RedisConnectionProvider.cs b/src/Orchard.Web/Modules/Orchard.Redis/Configuration/RedisConnectionProvider.cs new file mode 100644 index 000000000..e73bc365f --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Redis/Configuration/RedisConnectionProvider.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Concurrent; +using System.Configuration; +using Orchard.Environment.Configuration; +using Orchard.Logging; +using StackExchange.Redis; + +namespace Orchard.Redis.Configuration { + public class RedisConnectionProvider : IRedisConnectionProvider { + private static ConcurrentDictionary _connectionMultiplexers = new ConcurrentDictionary(); + private readonly ShellSettings _shellSettings; + + public RedisConnectionProvider(ShellSettings shellSettings) { + _shellSettings = shellSettings; + Logger = NullLogger.Instance; + } + + public ILogger Logger { get; set; } + + public string GetConnectionString(string service) { + var _tenantSettingsKey = _shellSettings.Name + ":" + service; + var _defaultSettingsKey = service; + + var connectionStringSettings = ConfigurationManager.ConnectionStrings[_tenantSettingsKey] ?? ConfigurationManager.ConnectionStrings[_defaultSettingsKey]; + + if (connectionStringSettings == null) { + throw new ConfigurationErrorsException("A connection string is expected for " + service); + } + + return connectionStringSettings.ConnectionString; + } + + public ConnectionMultiplexer GetConnection(string connectionString) { + + if (String.IsNullOrWhiteSpace(connectionString)) { + throw new ArgumentNullException("connectionString"); + } + + var connectionMultiplexer = _connectionMultiplexers.GetOrAdd(connectionString, cfg => { + Logger.Debug("Creating a new cache client for: {0}", connectionString); + return ConnectionMultiplexer.Connect(connectionString); + }); + + return connectionMultiplexer; + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Redis/MessageBus/RedisMessageBusBroker.cs b/src/Orchard.Web/Modules/Orchard.Redis/MessageBus/RedisMessageBusBroker.cs new file mode 100644 index 000000000..90b1d7d09 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Redis/MessageBus/RedisMessageBusBroker.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; +using Orchard.Environment.Configuration; +using Orchard.Environment.Extensions; +using Orchard.Logging; +using Orchard.MessageBus.Services; +using Orchard.Redis.Configuration; +using StackExchange.Redis; + +namespace Orchard.Redis.MessageBus { + + [OrchardFeature("Orchard.Redis.MessageBus")] + public class RedisMessageBusBroker : Component, IMessageBroker { + + private readonly IRedisConnectionProvider _redisConnectionProvider; + + public const string ConnectionStringKey = "Orchard.Redis.MessageBus"; + private readonly string _connectionString; + + private ConcurrentDictionary>> _handlers = new ConcurrentDictionary>>(); + + public RedisMessageBusBroker(ShellSettings shellSettings, IRedisConnectionProvider redisConnectionProvider) { + _redisConnectionProvider = redisConnectionProvider; + _connectionString = _redisConnectionProvider.GetConnectionString(ConnectionStringKey); + } + + public IDatabase Database { + get { + return _redisConnectionProvider.GetConnection(_connectionString).GetDatabase(); + } + } + + public void Subscribe(string channel, Action handler) { + + try { + var channelHandlers = _handlers.GetOrAdd(channel, c => { + return new ConcurrentBag>(); + }); + + channelHandlers.Add(handler); + + var sub = _redisConnectionProvider.GetConnection(_connectionString).GetSubscriber(); + sub.Subscribe(channel, (c, m) => { + + // the message contains the publisher before the first '/' + var messageTokens = m.ToString().Split('/'); + var publisher = messageTokens.FirstOrDefault(); + var message = messageTokens.Skip(1).FirstOrDefault(); + + if (String.IsNullOrWhiteSpace(publisher)) { + return; + } + + // ignore self sent messages + if (GetHostName().Equals(publisher, StringComparison.OrdinalIgnoreCase)) { + return; + } + + Logger.Debug("Processing {0}", message); + handler(c, message); + }); + + } + catch (Exception e) { + Logger.Error("An error occured while subscribing to " + channel, e); + } + } + + public void Publish(string channel, string message) { + Database.Publish(channel, GetHostName() + "/" + message); + } + + private string GetHostName() { + // use the current host and the process id as two servers could run on the same machine + return System.Net.Dns.GetHostName() + ":" + System.Diagnostics.Process.GetCurrentProcess().Id; + } + + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Redis/Module.txt b/src/Orchard.Web/Modules/Orchard.Redis/Module.txt new file mode 100644 index 000000000..7678a5459 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Redis/Module.txt @@ -0,0 +1,22 @@ +Name: Orchard.Redis +AntiForgery: enabled +Author: The Orchard Team +Website: http://orchardproject.net +Version: 1.0 +OrchardVersion: 1.0 +Description: Provides Redis integration with Orchard. +Features: + Orchard.Redis + Name: Redis + Description: Used to provide Redis related functionalities. + Category: Hosting + Orchard.Redis.MessageBus: + Name: Redis Message Bus + Description: A message bus implementation using Redis pub/sub. + Category: Hosting + Dependencies: Orchard.MessageBus, Orchard.Redis + Orchard.Redis.OutputCache: + Name: Redis Output Cache + Description: An output cache storage provider using Redis. + Category: Performance + Dependencies: Orchard.OutputCache, Orchard.Redis diff --git a/src/Orchard.Web/Modules/Orchard.Redis/Orchard.Redis.csproj b/src/Orchard.Web/Modules/Orchard.Redis/Orchard.Redis.csproj new file mode 100644 index 000000000..78355629e --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Redis/Orchard.Redis.csproj @@ -0,0 +1,153 @@ + + + + + Debug + AnyCPU + 9.0.30729 + 2.0 + {2C5EB8B3-A313-413D-BAA0-5C21D2A6EC6E} + {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} + Library + Properties + Orchard.MessageBus + Orchard.MessageBus + v4.5.1 + false + + + 4.0 + + + false + + + + + + + true + full + false + bin\ + DEBUG;TRACE + prompt + 4 + AllRules.ruleset + false + + + pdbonly + true + bin\ + TRACE + prompt + 4 + AllRules.ruleset + false + + + + + ..\..\..\..\lib\newtonsoft.json\Newtonsoft.Json.dll + + + False + ..\..\..\..\lib\nhibernate\NHibernate.dll + + + ..\..\..\..\lib\redis\StackExchange.Redis.dll + True + + + + + 3.5 + + + + False + ..\..\..\..\lib\aspnetmvc\System.Web.Mvc.dll + + + + + + + + + + + + + + + + + {2D1D92BB-4555-4CBE-8D0E-63563D6CE4C6} + Orchard.Framework + + + {9916839C-39FC-4CEB-A5AF-89CA7E87119F} + Orchard.Core + + + {ed715544-e649-4f48-b8ee-9368c41c3ac0} + Orchard.MessageBus + + + {6e444ff1-a47c-4cf6-bb3f-507c8ebd776d} + Orchard.OutputCache + + + + + + + + + + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + + + + $(ProjectDir)\..\Manifests + + + + + + + + + + + + False + True + 45979 + / + + + False + True + http://orchard.codeplex.com + False + + + + + \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Redis/OutputCache/RedisOutputCacheStorageProvider.cs b/src/Orchard.Web/Modules/Orchard.Redis/OutputCache/RedisOutputCacheStorageProvider.cs new file mode 100644 index 000000000..64a0d393d --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Redis/OutputCache/RedisOutputCacheStorageProvider.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using Orchard.Environment.Configuration; +using Orchard.Environment.Extensions; +using Orchard.Logging; +using Orchard.Redis.Configuration; +using Orchard.OutputCache.Models; +using Orchard.OutputCache.Services; +using StackExchange.Redis; + +namespace Orchard.Redis.OutputCache { + + [OrchardFeature("Orchard.Redis.OutputCache")] + [OrchardSuppressDependency("Orchard.OutputCache.Services.DefaultCacheStorageProvider")] + public class RedisOutputCacheStorageProvider : Component, IOutputCacheStorageProvider { + + private readonly ShellSettings _shellSettings; + private readonly IRedisConnectionProvider _redisConnectionProvider; + private HashSet _keysCache; + + public const string ConnectionStringKey = "Orchard.Redis.OutputCache"; + private readonly string _connectionString; + + public RedisOutputCacheStorageProvider(ShellSettings shellSettings, IRedisConnectionProvider redisConnectionProvider) { + _shellSettings = shellSettings; + _redisConnectionProvider = redisConnectionProvider; + _connectionString = _redisConnectionProvider.GetConnectionString(ConnectionStringKey); + + Logger = NullLogger.Instance; + } + + public IDatabase Database { + get { + return _redisConnectionProvider.GetConnection(_connectionString).GetDatabase(); + } + } + + public void Set(string key, CacheItem cacheItem) { + if (cacheItem.ValidFor <= 0) { + return; + } + + var value = JsonConvert.SerializeObject(cacheItem); + Database.StringSet(GetLocalizedKey(key), value, TimeSpan.FromSeconds(cacheItem.ValidFor)); + } + + public void Remove(string key) { + Database.KeyDelete(GetLocalizedKey(key)); + } + + public void RemoveAll() { + foreach (var key in GetAllKeys()) { + Remove(key); + } + } + + public CacheItem GetCacheItem(string key) { + string value = Database.StringGet(GetLocalizedKey(key)); + if(String.IsNullOrEmpty(value)) { + return null; + } + + return JsonConvert.DeserializeObject(value); + } + + public IEnumerable GetCacheItems(int skip, int count) { + foreach (var key in GetAllKeys().Skip(skip).Take(count)) { + var cacheItem = GetCacheItem(key); + // the item could have expired in the meantime + if (cacheItem != null) { + yield return cacheItem; + } + } + } + + public int GetCacheItemsCount() { + return GetAllKeys().Count(); + } + + /// + /// Creates a namespaced key to support multiple tenants on top of a single Redis connection. + /// + /// The key to localized. + /// A localized key based on the tenant name. + private string GetLocalizedKey(string key) { + return _shellSettings.Name + ":" + key; + } + + /// + /// Returns all the keys for the current tenant. + /// + /// The keys for the current tenant. + private IEnumerable GetAllKeys() { + // prevent the same request from computing the list twice (count + list) + if (_keysCache == null) { + _keysCache = new HashSet(); + var prefix = GetLocalizedKey(""); + + var connection = _redisConnectionProvider.GetConnection(_connectionString); + foreach (var endPoint in connection.GetEndPoints()) { + var server = connection.GetServer(endPoint); + foreach (var key in server.Keys(pattern: GetLocalizedKey("*"))) { + _keysCache.Add(key.ToString().Substring(prefix.Length)); + } + } + } + + return _keysCache; + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Redis/Properties/AssemblyInfo.cs b/src/Orchard.Web/Modules/Orchard.Redis/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..fc426464d --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Redis/Properties/AssemblyInfo.cs @@ -0,0 +1,37 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Security; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Orchard.Redis")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyProduct("Orchard")] +[assembly: AssemblyCopyright("")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("f4511dd5-71d3-438d-b5a2-42f0461833be")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Revision and Build Numbers +// by using the '*' as shown below: + +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] + diff --git a/src/Orchard.Web/Modules/Orchard.Redis/Web.config b/src/Orchard.Web/Modules/Orchard.Redis/Web.config new file mode 100644 index 000000000..6fc84bc30 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Redis/Web.config @@ -0,0 +1,41 @@ + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Orchard.sln b/src/Orchard.sln index 216369f8b..3a4ee4d07 100644 --- a/src/Orchard.sln +++ b/src/Orchard.sln @@ -245,6 +245,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.AuditTrail", "Orcha EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.MessageBus", "Orchard.Web\Modules\Orchard.MessageBus\Orchard.MessageBus.csproj", "{ED715544-E649-4F48-B8EE-9368C41C3AC0}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.Redis", "Orchard.Web\Modules\Orchard.Redis\Orchard.Redis.csproj", "{2C5EB8B3-A313-413D-BAA0-5C21D2A6EC6E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution CodeCoverage|Any CPU = CodeCoverage|Any CPU @@ -1017,6 +1019,16 @@ Global {ED715544-E649-4F48-B8EE-9368C41C3AC0}.FxCop|Any CPU.ActiveCfg = Release|Any CPU {ED715544-E649-4F48-B8EE-9368C41C3AC0}.Release|Any CPU.ActiveCfg = Release|Any CPU {ED715544-E649-4F48-B8EE-9368C41C3AC0}.Release|Any CPU.Build.0 = Release|Any CPU + {2C5EB8B3-A313-413D-BAA0-5C21D2A6EC6E}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU + {2C5EB8B3-A313-413D-BAA0-5C21D2A6EC6E}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU + {2C5EB8B3-A313-413D-BAA0-5C21D2A6EC6E}.Coverage|Any CPU.ActiveCfg = Release|Any CPU + {2C5EB8B3-A313-413D-BAA0-5C21D2A6EC6E}.Coverage|Any CPU.Build.0 = Release|Any CPU + {2C5EB8B3-A313-413D-BAA0-5C21D2A6EC6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2C5EB8B3-A313-413D-BAA0-5C21D2A6EC6E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2C5EB8B3-A313-413D-BAA0-5C21D2A6EC6E}.FxCop|Any CPU.ActiveCfg = Release|Any CPU + {2C5EB8B3-A313-413D-BAA0-5C21D2A6EC6E}.FxCop|Any CPU.Build.0 = Release|Any CPU + {2C5EB8B3-A313-413D-BAA0-5C21D2A6EC6E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2C5EB8B3-A313-413D-BAA0-5C21D2A6EC6E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1097,5 +1109,6 @@ Global {14A96B1A-9DC9-44C8-A675-206329E15263} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5} {3DD574CD-9C5D-4A45-85E1-EBBA64C22B5F} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5} {ED715544-E649-4F48-B8EE-9368C41C3AC0} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5} + {2C5EB8B3-A313-413D-BAA0-5C21D2A6EC6E} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5} EndGlobalSection EndGlobal From dc6087e62043cc3301c1c7ef1db327211c19b00a Mon Sep 17 00:00:00 2001 From: Piotr Szmyd Date: Wed, 15 Oct 2014 02:02:36 +0200 Subject: [PATCH 03/10] Making ICacheService and ICacheServiceProviders generic. --- .../Services/DefaultCacheService.cs | 10 +++++----- .../Services/DefaultCacheStorageProvider.cs | 13 +++++++++---- .../Orchard.Caching/Services/ICacheService.cs | 19 +++++++------------ .../Services/ICacheStorageProvider.cs | 6 +++--- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/Orchard.Web/Modules/Orchard.Caching/Services/DefaultCacheService.cs b/src/Orchard.Web/Modules/Orchard.Caching/Services/DefaultCacheService.cs index 378f576b3..af53e4b65 100644 --- a/src/Orchard.Web/Modules/Orchard.Caching/Services/DefaultCacheService.cs +++ b/src/Orchard.Web/Modules/Orchard.Caching/Services/DefaultCacheService.cs @@ -17,15 +17,15 @@ namespace Orchard.Caching.Services { _prefix = shellSettings.Name; } - public object Get(string key) { - return _cacheStorageProvider.Get(BuildFullKey(key)); + public T Get(string key) { + return _cacheStorageProvider.Get(BuildFullKey(key)); } - public void Put(string key, object value) { + public void Put(string key, T value) { _cacheStorageProvider.Put(BuildFullKey(key), value); } - public void Put(string key, object value, TimeSpan validFor) { + public void Put(string key, T value, TimeSpan validFor) { _cacheStorageProvider.Put(BuildFullKey(key), value, validFor); } @@ -38,7 +38,7 @@ namespace Orchard.Caching.Services { } private string BuildFullKey(string key) { - return String.Concat(_prefix, "_", key); + return String.Concat(_prefix, ":", key); } } } \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Caching/Services/DefaultCacheStorageProvider.cs b/src/Orchard.Web/Modules/Orchard.Caching/Services/DefaultCacheStorageProvider.cs index 47e8e66aa..a5d3c1c6f 100644 --- a/src/Orchard.Web/Modules/Orchard.Caching/Services/DefaultCacheStorageProvider.cs +++ b/src/Orchard.Web/Modules/Orchard.Caching/Services/DefaultCacheStorageProvider.cs @@ -6,7 +6,7 @@ using System.Web; namespace Orchard.Caching.Services { public class DefaultCacheStorageProvider : ICacheStorageProvider { - public void Put(string key, object value) { + public void Put(string key, T value) { HttpRuntime.Cache.Insert( key, value, @@ -17,7 +17,7 @@ namespace Orchard.Caching.Services { null); } - public void Put(string key, object value, TimeSpan validFor) { + public void Put(string key, T value, TimeSpan validFor) { HttpRuntime.Cache.Insert( key, value, @@ -44,8 +44,13 @@ namespace Orchard.Caching.Services { } } - public object Get(string key) { - return HttpRuntime.Cache.Get(key); + public T Get(string key) { + var value = HttpRuntime.Cache.Get(key); + if (value is T) { + return (T)value; + } + + return default(T); } } } \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Caching/Services/ICacheService.cs b/src/Orchard.Web/Modules/Orchard.Caching/Services/ICacheService.cs index 74ee9d39a..03aeede41 100644 --- a/src/Orchard.Web/Modules/Orchard.Caching/Services/ICacheService.cs +++ b/src/Orchard.Web/Modules/Orchard.Caching/Services/ICacheService.cs @@ -2,23 +2,18 @@ namespace Orchard.Caching.Services { public interface ICacheService : IDependency { + T Get(string key); - object Get(string key); - void Put(string key, object value); - void Put(string key, object value, TimeSpan validFor); + void Put(string key, T value); + void Put(string key, T value, TimeSpan validFor); void Remove(string key); void Clear(); } public static class CachingExtensions { - - public static T Get(this ICacheService cacheService, string key) { - return (T)cacheService.Get(key); - } - public static T Get(this ICacheService cacheService, string key, Func factory) { - var result = cacheService.Get(key); + var result = cacheService.Get(key); if (result == null) { var computed = factory(); cacheService.Put(key, computed); @@ -26,11 +21,11 @@ namespace Orchard.Caching.Services { } // try to convert to T - return (T)result; + return result; } public static T Get(this ICacheService cacheService, string key, Func factory, TimeSpan validFor) { - var result = cacheService.Get(key); + var result = cacheService.Get(key); if (result == null) { var computed = factory(); cacheService.Put(key, computed, validFor); @@ -38,7 +33,7 @@ namespace Orchard.Caching.Services { } // try to convert to T - return (T)result; + return result; } } } \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Caching/Services/ICacheStorageProvider.cs b/src/Orchard.Web/Modules/Orchard.Caching/Services/ICacheStorageProvider.cs index 545b4f0df..60253600f 100644 --- a/src/Orchard.Web/Modules/Orchard.Caching/Services/ICacheStorageProvider.cs +++ b/src/Orchard.Web/Modules/Orchard.Caching/Services/ICacheStorageProvider.cs @@ -2,9 +2,9 @@ namespace Orchard.Caching.Services { public interface ICacheStorageProvider : IDependency { - object Get(string key); - void Put(string key, object value); - void Put(string key, object value, TimeSpan validFor); + T Get(string key); + void Put(string key, T value); + void Put(string key, T value, TimeSpan validFor); void Remove(string key); void Clear(); } From 044746d51131246cfd03a995e1fa3ba062c3efa3 Mon Sep 17 00:00:00 2001 From: Piotr Szmyd Date: Wed, 15 Oct 2014 09:39:06 +0200 Subject: [PATCH 04/10] Lua scripts for efficient batch counting and removing keys. --- .../Extensions/RedisDatabaseExtensions.cs | 42 +++++++++++++++++++ .../Orchard.Redis/Orchard.Redis.csproj | 1 + 2 files changed, 43 insertions(+) create mode 100644 src/Orchard.Web/Modules/Orchard.Redis/Extensions/RedisDatabaseExtensions.cs diff --git a/src/Orchard.Web/Modules/Orchard.Redis/Extensions/RedisDatabaseExtensions.cs b/src/Orchard.Web/Modules/Orchard.Redis/Extensions/RedisDatabaseExtensions.cs new file mode 100644 index 000000000..7d8b68ef1 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Redis/Extensions/RedisDatabaseExtensions.cs @@ -0,0 +1,42 @@ +using System; +using StackExchange.Redis; + +namespace Orchard.Redis.Extensions +{ + public static class RedisDatabaseExtensions + { + public static void KeyDeleteWithPrefix(this IDatabase database, string prefix) + { + if (database == null) { + throw new ArgumentException("Database cannot be null", "database"); + } + + if (string.IsNullOrWhiteSpace(prefix)) { + throw new ArgumentException("Prefix cannot be empty", "database"); + } + + database.ScriptEvaluate(@" + local keys = redis.call('keys', ARGV[1]) + for i=1,#keys,5000 do + redis.call('del', unpack(keys, i, math.min(i+4999, #keys))) + end", values: new RedisValue[] { prefix }); + } + + public static int KeyCount(this IDatabase database, string prefix) { + if (database == null) { + throw new ArgumentException("Database cannot be null", "database"); + } + + if (string.IsNullOrWhiteSpace(prefix)) { + throw new ArgumentException("Prefix cannot be empty", "database"); + } + + var retVal = database.ScriptEvaluate("return table.getn(redis.call('keys', ARGV[1]))", values: new RedisValue[] { prefix }); + + if (retVal.IsNull) + return 0; + + return (int)retVal; + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Redis/Orchard.Redis.csproj b/src/Orchard.Web/Modules/Orchard.Redis/Orchard.Redis.csproj index 78355629e..debe8081b 100644 --- a/src/Orchard.Web/Modules/Orchard.Redis/Orchard.Redis.csproj +++ b/src/Orchard.Web/Modules/Orchard.Redis/Orchard.Redis.csproj @@ -101,6 +101,7 @@ + From b79f2e2db2635a80bcaccaad094f1ccc267b6af4 Mon Sep 17 00:00:00 2001 From: Piotr Szmyd Date: Wed, 15 Oct 2014 09:40:28 +0200 Subject: [PATCH 05/10] Implemented Redis-based cache provider. --- .../Caching/RedisCacheStorageProvider.cs | 69 +++++++++++++++++++ .../Modules/Orchard.Redis/Module.txt | 5 ++ .../Orchard.Redis/Orchard.Redis.csproj | 5 ++ 3 files changed, 79 insertions(+) create mode 100644 src/Orchard.Web/Modules/Orchard.Redis/Caching/RedisCacheStorageProvider.cs diff --git a/src/Orchard.Web/Modules/Orchard.Redis/Caching/RedisCacheStorageProvider.cs b/src/Orchard.Web/Modules/Orchard.Redis/Caching/RedisCacheStorageProvider.cs new file mode 100644 index 000000000..af32bb6f9 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Redis/Caching/RedisCacheStorageProvider.cs @@ -0,0 +1,69 @@ +using Newtonsoft.Json; +using Orchard; +using Orchard.Caching.Services; +using Orchard.Environment.Configuration; +using Orchard.Environment.Extensions; +using Orchard.Logging; +using Orchard.Redis.Configuration; +using StackExchange.Redis; +using System; + +namespace Orchard.Redis.Caching +{ + [OrchardFeature("Orchard.Redis.Caching")] + [OrchardSuppressDependency("Orchard.Caching.Services.DefaultCacheStorageProvider")] + public class RedisCacheStorageProvider : Component, ICacheStorageProvider + { + public const string ConnectionStringKey = "Orchard.Redis.Cache"; + + private readonly ShellSettings _shellSettings; + private readonly IRedisConnectionProvider _redisConnectionProvider; + private readonly string _connectionString; + + public IDatabase Database { + get { + return _redisConnectionProvider.GetConnection(_connectionString).GetDatabase(); + } + } + + public RedisCacheStorageProvider(ShellSettings shellSettings, IRedisConnectionProvider redisConnectionProvider) + { + _shellSettings = shellSettings; + _redisConnectionProvider = redisConnectionProvider; + _connectionString = _redisConnectionProvider.GetConnectionString(ConnectionStringKey); + Logger = NullLogger.Instance; + } + + public T Get(string key) { + var json = Database.StringGet(GetLocalizedKey(key)); + return JsonConvert.DeserializeObject(json); + } + + public void Put(string key, T value) + { + var json = JsonConvert.SerializeObject(value); + Database.StringSet(GetLocalizedKey(key), json, null); + } + + public void Put(string key, T value, TimeSpan validFor) + { + var json = JsonConvert.SerializeObject(value); + Database.StringSet(GetLocalizedKey(key), json, validFor); + } + + public void Remove(string key) + { + Database.KeyDelete(key); + } + + public void Clear() + { + Database.KeyDeleteWithPrefix(GetLocalizedKey("*")); + } + + private string GetLocalizedKey(string key) + { + return _shellSettings.Name + ":Cache:" + key; + } + } +} diff --git a/src/Orchard.Web/Modules/Orchard.Redis/Module.txt b/src/Orchard.Web/Modules/Orchard.Redis/Module.txt index 7678a5459..88c3e2524 100644 --- a/src/Orchard.Web/Modules/Orchard.Redis/Module.txt +++ b/src/Orchard.Web/Modules/Orchard.Redis/Module.txt @@ -20,3 +20,8 @@ Features: Description: An output cache storage provider using Redis. Category: Performance Dependencies: Orchard.OutputCache, Orchard.Redis + Orchard.Redis.Caching: + Name: Redis Cache + Description: Business data cache using Redis. + Category: Performance + Dependencies: Orchard.Caching, Orchard.Redis diff --git a/src/Orchard.Web/Modules/Orchard.Redis/Orchard.Redis.csproj b/src/Orchard.Web/Modules/Orchard.Redis/Orchard.Redis.csproj index debe8081b..9febf2aae 100644 --- a/src/Orchard.Web/Modules/Orchard.Redis/Orchard.Redis.csproj +++ b/src/Orchard.Web/Modules/Orchard.Redis/Orchard.Redis.csproj @@ -91,6 +91,10 @@ {9916839C-39FC-4CEB-A5AF-89CA7E87119F} Orchard.Core + + {7528BF74-25C7-4ABE-883A-443B4EEC4776} + Orchard.Caching + {ed715544-e649-4f48-b8ee-9368c41c3ac0} Orchard.MessageBus @@ -101,6 +105,7 @@ + From 3b6f978100de72d8f537abf7016ff869d399cd6d Mon Sep 17 00:00:00 2001 From: Piotr Szmyd Date: Wed, 15 Oct 2014 09:43:23 +0200 Subject: [PATCH 06/10] Refactored output cache provider to use atomic, Lua script-based key deletion. --- .../OutputCache/RedisOutputCacheStorageProvider.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Orchard.Web/Modules/Orchard.Redis/OutputCache/RedisOutputCacheStorageProvider.cs b/src/Orchard.Web/Modules/Orchard.Redis/OutputCache/RedisOutputCacheStorageProvider.cs index 64a0d393d..8a937b3bb 100644 --- a/src/Orchard.Web/Modules/Orchard.Redis/OutputCache/RedisOutputCacheStorageProvider.cs +++ b/src/Orchard.Web/Modules/Orchard.Redis/OutputCache/RedisOutputCacheStorageProvider.cs @@ -8,6 +8,7 @@ using Orchard.Logging; using Orchard.Redis.Configuration; using Orchard.OutputCache.Models; using Orchard.OutputCache.Services; +using Orchard.Redis.Extensions; using StackExchange.Redis; namespace Orchard.Redis.OutputCache { @@ -51,9 +52,7 @@ namespace Orchard.Redis.OutputCache { } public void RemoveAll() { - foreach (var key in GetAllKeys()) { - Remove(key); - } + Database.KeyDeleteWithPrefix(GetLocalizedKey("*")); } public CacheItem GetCacheItem(string key) { @@ -85,7 +84,7 @@ namespace Orchard.Redis.OutputCache { /// The key to localized. /// A localized key based on the tenant name. private string GetLocalizedKey(string key) { - return _shellSettings.Name + ":" + key; + return _shellSettings.Name + ":OutputCache:" + key; } /// From 219150bd20fe927a69ab1b83115d02fc950627a0 Mon Sep 17 00:00:00 2001 From: Piotr Szmyd Date: Wed, 15 Oct 2014 09:45:06 +0200 Subject: [PATCH 07/10] Fixed solution file and corrected namespace in Orchard.Redis --- src/Orchard.Web/Modules/Orchard.Redis/Orchard.Redis.csproj | 4 ++-- src/Orchard.sln | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Orchard.Web/Modules/Orchard.Redis/Orchard.Redis.csproj b/src/Orchard.Web/Modules/Orchard.Redis/Orchard.Redis.csproj index 9febf2aae..c9db8f9c7 100644 --- a/src/Orchard.Web/Modules/Orchard.Redis/Orchard.Redis.csproj +++ b/src/Orchard.Web/Modules/Orchard.Redis/Orchard.Redis.csproj @@ -10,8 +10,8 @@ {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} Library Properties - Orchard.MessageBus - Orchard.MessageBus + Orchard.Redis + Orchard.Redis v4.5.1 false diff --git a/src/Orchard.sln b/src/Orchard.sln index 766915109..a88be2f7d 100644 --- a/src/Orchard.sln +++ b/src/Orchard.sln @@ -1132,5 +1132,9 @@ Global {7528BF74-25C7-4ABE-883A-443B4EEC4776} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5} {14A96B1A-9DC9-44C8-A675-206329E15263} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5} {3DD574CD-9C5D-4A45-85E1-EBBA64C22B5F} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5} + {ED715544-E649-4F48-B8EE-9368C41C3AC0} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5} + {2C5EB8B3-A313-413D-BAA0-5C21D2A6EC6E} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5} + {6BD8B2FA-F2E3-4AC8-A4C3-2925A653889A} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5} + {82190F52-2901-46D6-8A4C-34649959483F} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5} EndGlobalSection EndGlobal From d7628aaf613f09a5004961f9fd84d69a8be2ae1f Mon Sep 17 00:00:00 2001 From: Piotr Szmyd Date: Wed, 15 Oct 2014 09:45:06 +0200 Subject: [PATCH 08/10] Moving extension methods to separate namespace. --- .../Modules/Orchard.Redis/Caching/RedisCacheStorageProvider.cs | 1 + src/Orchard.Web/Modules/Orchard.Redis/Orchard.Redis.csproj | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Orchard.Web/Modules/Orchard.Redis/Caching/RedisCacheStorageProvider.cs b/src/Orchard.Web/Modules/Orchard.Redis/Caching/RedisCacheStorageProvider.cs index af32bb6f9..350c2ae11 100644 --- a/src/Orchard.Web/Modules/Orchard.Redis/Caching/RedisCacheStorageProvider.cs +++ b/src/Orchard.Web/Modules/Orchard.Redis/Caching/RedisCacheStorageProvider.cs @@ -5,6 +5,7 @@ using Orchard.Environment.Configuration; using Orchard.Environment.Extensions; using Orchard.Logging; using Orchard.Redis.Configuration; +using Orchard.Redis.Extensions; using StackExchange.Redis; using System; diff --git a/src/Orchard.Web/Modules/Orchard.Redis/Orchard.Redis.csproj b/src/Orchard.Web/Modules/Orchard.Redis/Orchard.Redis.csproj index c9db8f9c7..4590be157 100644 --- a/src/Orchard.Web/Modules/Orchard.Redis/Orchard.Redis.csproj +++ b/src/Orchard.Web/Modules/Orchard.Redis/Orchard.Redis.csproj @@ -106,7 +106,7 @@ - + From b77f3a4c24ab95736c19e8f83ec27101c779f2fd Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Tue, 21 Oct 2014 21:32:47 -0700 Subject: [PATCH 09/10] Fixing style --- .../Caching/RedisCacheStorageProvider.cs | 102 ++++++++---------- .../Configuration/IRedisConnectionProvider.cs | 4 +- .../Configuration/RedisConnectionProvider.cs | 1 + .../Extensions/RedisDatabaseExtensions.cs | 12 +-- .../Modules/Orchard.Redis/Module.txt | 8 +- .../RedisOutputCacheStorageProvider.cs | 4 +- 6 files changed, 61 insertions(+), 70 deletions(-) diff --git a/src/Orchard.Web/Modules/Orchard.Redis/Caching/RedisCacheStorageProvider.cs b/src/Orchard.Web/Modules/Orchard.Redis/Caching/RedisCacheStorageProvider.cs index 350c2ae11..edbe6ced3 100644 --- a/src/Orchard.Web/Modules/Orchard.Redis/Caching/RedisCacheStorageProvider.cs +++ b/src/Orchard.Web/Modules/Orchard.Redis/Caching/RedisCacheStorageProvider.cs @@ -1,5 +1,4 @@ using Newtonsoft.Json; -using Orchard; using Orchard.Caching.Services; using Orchard.Environment.Configuration; using Orchard.Environment.Extensions; @@ -9,62 +8,55 @@ using Orchard.Redis.Extensions; using StackExchange.Redis; using System; -namespace Orchard.Redis.Caching -{ - [OrchardFeature("Orchard.Redis.Caching")] - [OrchardSuppressDependency("Orchard.Caching.Services.DefaultCacheStorageProvider")] - public class RedisCacheStorageProvider : Component, ICacheStorageProvider - { - public const string ConnectionStringKey = "Orchard.Redis.Cache"; +namespace Orchard.Redis.Caching { - private readonly ShellSettings _shellSettings; - private readonly IRedisConnectionProvider _redisConnectionProvider; - private readonly string _connectionString; + [OrchardFeature("Orchard.Redis.Caching")] + [OrchardSuppressDependency("Orchard.Caching.Services.DefaultCacheStorageProvider")] + public class RedisCacheStorageProvider : Component, ICacheStorageProvider { + public const string ConnectionStringKey = "Orchard.Redis.Cache"; - public IDatabase Database { - get { - return _redisConnectionProvider.GetConnection(_connectionString).GetDatabase(); + private readonly ShellSettings _shellSettings; + private readonly IRedisConnectionProvider _redisConnectionProvider; + private readonly string _connectionString; + + public IDatabase Database { + get { + return _redisConnectionProvider.GetConnection(_connectionString).GetDatabase(); + } + } + + public RedisCacheStorageProvider(ShellSettings shellSettings, IRedisConnectionProvider redisConnectionProvider) { + _shellSettings = shellSettings; + _redisConnectionProvider = redisConnectionProvider; + _connectionString = _redisConnectionProvider.GetConnectionString(ConnectionStringKey); + Logger = NullLogger.Instance; + } + + public T Get(string key) { + var json = Database.StringGet(GetLocalizedKey(key)); + return JsonConvert.DeserializeObject(json); + } + + public void Put(string key, T value) { + var json = JsonConvert.SerializeObject(value); + Database.StringSet(GetLocalizedKey(key), json, null); + } + + public void Put(string key, T value, TimeSpan validFor) { + var json = JsonConvert.SerializeObject(value); + Database.StringSet(GetLocalizedKey(key), json, validFor); + } + + public void Remove(string key) { + Database.KeyDelete(key); + } + + public void Clear() { + Database.KeyDeleteWithPrefix(GetLocalizedKey("*")); + } + + private string GetLocalizedKey(string key) { + return _shellSettings.Name + ":Cache:" + key; } } - - public RedisCacheStorageProvider(ShellSettings shellSettings, IRedisConnectionProvider redisConnectionProvider) - { - _shellSettings = shellSettings; - _redisConnectionProvider = redisConnectionProvider; - _connectionString = _redisConnectionProvider.GetConnectionString(ConnectionStringKey); - Logger = NullLogger.Instance; - } - - public T Get(string key) { - var json = Database.StringGet(GetLocalizedKey(key)); - return JsonConvert.DeserializeObject(json); - } - - public void Put(string key, T value) - { - var json = JsonConvert.SerializeObject(value); - Database.StringSet(GetLocalizedKey(key), json, null); - } - - public void Put(string key, T value, TimeSpan validFor) - { - var json = JsonConvert.SerializeObject(value); - Database.StringSet(GetLocalizedKey(key), json, validFor); - } - - public void Remove(string key) - { - Database.KeyDelete(key); - } - - public void Clear() - { - Database.KeyDeleteWithPrefix(GetLocalizedKey("*")); - } - - private string GetLocalizedKey(string key) - { - return _shellSettings.Name + ":Cache:" + key; - } - } } diff --git a/src/Orchard.Web/Modules/Orchard.Redis/Configuration/IRedisConnectionProvider.cs b/src/Orchard.Web/Modules/Orchard.Redis/Configuration/IRedisConnectionProvider.cs index 5caeba2a1..0139191b8 100644 --- a/src/Orchard.Web/Modules/Orchard.Redis/Configuration/IRedisConnectionProvider.cs +++ b/src/Orchard.Web/Modules/Orchard.Redis/Configuration/IRedisConnectionProvider.cs @@ -1,10 +1,10 @@ using StackExchange.Redis; namespace Orchard.Redis.Configuration { + public interface IRedisConnectionProvider : ISingletonDependency { - ConnectionMultiplexer GetConnection(string connectionString); - string GetConnectionString(string service); } + } \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Redis/Configuration/RedisConnectionProvider.cs b/src/Orchard.Web/Modules/Orchard.Redis/Configuration/RedisConnectionProvider.cs index e73bc365f..e3e994e5a 100644 --- a/src/Orchard.Web/Modules/Orchard.Redis/Configuration/RedisConnectionProvider.cs +++ b/src/Orchard.Web/Modules/Orchard.Redis/Configuration/RedisConnectionProvider.cs @@ -6,6 +6,7 @@ using Orchard.Logging; using StackExchange.Redis; namespace Orchard.Redis.Configuration { + public class RedisConnectionProvider : IRedisConnectionProvider { private static ConcurrentDictionary _connectionMultiplexers = new ConcurrentDictionary(); private readonly ShellSettings _shellSettings; diff --git a/src/Orchard.Web/Modules/Orchard.Redis/Extensions/RedisDatabaseExtensions.cs b/src/Orchard.Web/Modules/Orchard.Redis/Extensions/RedisDatabaseExtensions.cs index 7d8b68ef1..11ead13c5 100644 --- a/src/Orchard.Web/Modules/Orchard.Redis/Extensions/RedisDatabaseExtensions.cs +++ b/src/Orchard.Web/Modules/Orchard.Redis/Extensions/RedisDatabaseExtensions.cs @@ -1,12 +1,9 @@ using System; using StackExchange.Redis; -namespace Orchard.Redis.Extensions -{ - public static class RedisDatabaseExtensions - { - public static void KeyDeleteWithPrefix(this IDatabase database, string prefix) - { +namespace Orchard.Redis.Extensions { + public static class RedisDatabaseExtensions { + public static void KeyDeleteWithPrefix(this IDatabase database, string prefix) { if (database == null) { throw new ArgumentException("Database cannot be null", "database"); } @@ -33,8 +30,9 @@ namespace Orchard.Redis.Extensions var retVal = database.ScriptEvaluate("return table.getn(redis.call('keys', ARGV[1]))", values: new RedisValue[] { prefix }); - if (retVal.IsNull) + if (retVal.IsNull) { return 0; + } return (int)retVal; } diff --git a/src/Orchard.Web/Modules/Orchard.Redis/Module.txt b/src/Orchard.Web/Modules/Orchard.Redis/Module.txt index 88c3e2524..86d7bf66f 100644 --- a/src/Orchard.Web/Modules/Orchard.Redis/Module.txt +++ b/src/Orchard.Web/Modules/Orchard.Redis/Module.txt @@ -6,7 +6,7 @@ Version: 1.0 OrchardVersion: 1.0 Description: Provides Redis integration with Orchard. Features: - Orchard.Redis + Orchard.Redis Name: Redis Description: Used to provide Redis related functionalities. Category: Hosting @@ -14,13 +14,13 @@ Features: Name: Redis Message Bus Description: A message bus implementation using Redis pub/sub. Category: Hosting - Dependencies: Orchard.MessageBus, Orchard.Redis - Orchard.Redis.OutputCache: + Dependencies: Orchard.MessageBus, Orchard.Redis + Orchard.Redis.OutputCache: Name: Redis Output Cache Description: An output cache storage provider using Redis. Category: Performance Dependencies: Orchard.OutputCache, Orchard.Redis - Orchard.Redis.Caching: + Orchard.Redis.Caching: Name: Redis Cache Description: Business data cache using Redis. Category: Performance diff --git a/src/Orchard.Web/Modules/Orchard.Redis/OutputCache/RedisOutputCacheStorageProvider.cs b/src/Orchard.Web/Modules/Orchard.Redis/OutputCache/RedisOutputCacheStorageProvider.cs index 8a937b3bb..4fdeac39f 100644 --- a/src/Orchard.Web/Modules/Orchard.Redis/OutputCache/RedisOutputCacheStorageProvider.cs +++ b/src/Orchard.Web/Modules/Orchard.Redis/OutputCache/RedisOutputCacheStorageProvider.cs @@ -46,7 +46,7 @@ namespace Orchard.Redis.OutputCache { var value = JsonConvert.SerializeObject(cacheItem); Database.StringSet(GetLocalizedKey(key), value, TimeSpan.FromSeconds(cacheItem.ValidFor)); } - + public void Remove(string key) { Database.KeyDelete(GetLocalizedKey(key)); } @@ -57,7 +57,7 @@ namespace Orchard.Redis.OutputCache { public CacheItem GetCacheItem(string key) { string value = Database.StringGet(GetLocalizedKey(key)); - if(String.IsNullOrEmpty(value)) { + if (String.IsNullOrEmpty(value)) { return null; } From 15740d1aadd0760eea2df9b2d1800b148556674d Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Tue, 21 Oct 2014 21:45:22 -0700 Subject: [PATCH 10/10] Optimizing keys count --- .../Modules/Orchard.Redis/Extensions/RedisDatabaseExtensions.cs | 1 + .../OutputCache/RedisOutputCacheStorageProvider.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Orchard.Web/Modules/Orchard.Redis/Extensions/RedisDatabaseExtensions.cs b/src/Orchard.Web/Modules/Orchard.Redis/Extensions/RedisDatabaseExtensions.cs index 11ead13c5..2b223a064 100644 --- a/src/Orchard.Web/Modules/Orchard.Redis/Extensions/RedisDatabaseExtensions.cs +++ b/src/Orchard.Web/Modules/Orchard.Redis/Extensions/RedisDatabaseExtensions.cs @@ -3,6 +3,7 @@ using StackExchange.Redis; namespace Orchard.Redis.Extensions { public static class RedisDatabaseExtensions { + public static void KeyDeleteWithPrefix(this IDatabase database, string prefix) { if (database == null) { throw new ArgumentException("Database cannot be null", "database"); diff --git a/src/Orchard.Web/Modules/Orchard.Redis/OutputCache/RedisOutputCacheStorageProvider.cs b/src/Orchard.Web/Modules/Orchard.Redis/OutputCache/RedisOutputCacheStorageProvider.cs index 4fdeac39f..85cde139e 100644 --- a/src/Orchard.Web/Modules/Orchard.Redis/OutputCache/RedisOutputCacheStorageProvider.cs +++ b/src/Orchard.Web/Modules/Orchard.Redis/OutputCache/RedisOutputCacheStorageProvider.cs @@ -75,7 +75,7 @@ namespace Orchard.Redis.OutputCache { } public int GetCacheItemsCount() { - return GetAllKeys().Count(); + return Database.KeyCount(GetLocalizedKey("*")); } ///