From 7a2d4c4a03ee4409981402d756d569616cb4505e Mon Sep 17 00:00:00 2001 From: loudej Date: Fri, 27 Nov 2009 08:52:38 +0000 Subject: [PATCH] Adding a background task sweep generator. Updated cms page scheduler processing as a background task. --HG-- extra : convert_revision : svn%3A5ff7c347-ad56-4c35-b696-ccb81de16e03/trunk%4042426 --- .../Environment/DefaultOrchardShellTests.cs | 3 +- src/Orchard.Tests/EventsTests.cs | 77 +++++++++++++++ src/Orchard.Tests/Orchard.Tests.csproj | 2 + .../Tasks/SweepGeneratorTests.cs | 43 ++++++++ .../Services/PageScheduler.cs | 4 +- src/Orchard/Data/HackSessionLocator.cs | 6 +- .../Environment/DefaultOrchardShell.cs | 23 ++++- src/Orchard/Environment/IOrchardShell.cs | 1 + .../Environment/IOrchardShellEvents.cs | 6 ++ src/Orchard/IEvents.cs | 24 +++++ src/Orchard/Orchard.csproj | 5 + src/Orchard/Tasks/BackgroundService.cs | 25 +++++ src/Orchard/Tasks/IBackgroundTask.cs | 5 + src/Orchard/Tasks/SweepGenerator.cs | 97 +++++++++++++++++++ 14 files changed, 313 insertions(+), 8 deletions(-) create mode 100644 src/Orchard.Tests/EventsTests.cs create mode 100644 src/Orchard.Tests/Tasks/SweepGeneratorTests.cs create mode 100644 src/Orchard/Environment/IOrchardShellEvents.cs create mode 100644 src/Orchard/IEvents.cs create mode 100644 src/Orchard/Tasks/BackgroundService.cs create mode 100644 src/Orchard/Tasks/IBackgroundTask.cs create mode 100644 src/Orchard/Tasks/SweepGenerator.cs diff --git a/src/Orchard.Tests/Environment/DefaultOrchardShellTests.cs b/src/Orchard.Tests/Environment/DefaultOrchardShellTests.cs index c51dd089a..781a48fd0 100644 --- a/src/Orchard.Tests/Environment/DefaultOrchardShellTests.cs +++ b/src/Orchard.Tests/Environment/DefaultOrchardShellTests.cs @@ -38,7 +38,8 @@ namespace Orchard.Tests.Environment { new[] { modelBinderProvider1, modelBinderProvider2 }, modelBinderPublisher, new ViewEngineCollection(), - new Moq.Mock().Object); + new Moq.Mock().Object, + Enumerable.Empty()); runtime.Activate(); diff --git a/src/Orchard.Tests/EventsTests.cs b/src/Orchard.Tests/EventsTests.cs new file mode 100644 index 000000000..fe2846508 --- /dev/null +++ b/src/Orchard.Tests/EventsTests.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NUnit.Framework; +using Orchard.Logging; + +namespace Orchard.Tests { + [TestFixture] + public class EventsTests { + [Test] + public void AllEventsAreCalled() { + var events = new ITestEvents[] { new FooSink(), new BarSink() }; + + events.Invoke(x => x.Hello("world"), NullLogger.Instance); + + Assert.That(events.OfType().Single().Name, Is.EqualTo("world")); + Assert.That(events.OfType().Single().Name, Is.EqualTo("world")); + } + + [Test] + public void AnExceptionShouldBeLoggedAndOtherEventsWillBeFired() { + var events = new ITestEvents[] { new FooSink(), new CrashSink(), new BarSink() }; + + var logger = new TestLogger(); + + events.Invoke(x => x.Hello("world"), logger); + + Assert.That(events.OfType().Single().Name, Is.EqualTo("world")); + Assert.That(events.OfType().Single().Name, Is.EqualTo("world")); + Assert.That(logger.LogException, Is.TypeOf()); + Assert.That(logger.LogException, Has.Property("Message").EqualTo("Illegal name 'world'")); + } + + private class TestLogger : ILogger { + public bool IsEnabled(LogLevel level) { + return true; + } + + public void Log(LogLevel level, Exception exception, string format, params object[] args) { + LogException = exception; + LogFormat = format; + LogArgs = args; + } + + public Exception LogException { get; set; } + public string LogFormat { get; set; } + public object[] LogArgs { get; set; } + } + + private interface ITestEvents : IEvents { + void Hello(string name); + } + + private class FooSink : ITestEvents { + void ITestEvents.Hello(string name) { + Name = name; + } + + public string Name { get; set; } + } + + private class BarSink : ITestEvents { + void ITestEvents.Hello(string name) { + Name = name; + } + public string Name { get; set; } + } + + + private class CrashSink : ITestEvents { + void ITestEvents.Hello(string name) { + throw new ApplicationException("Illegal name '" + name + "'"); + } + } + } +} diff --git a/src/Orchard.Tests/Orchard.Tests.csproj b/src/Orchard.Tests/Orchard.Tests.csproj index 6f468217e..f169410ea 100644 --- a/src/Orchard.Tests/Orchard.Tests.csproj +++ b/src/Orchard.Tests/Orchard.Tests.csproj @@ -104,6 +104,7 @@ + @@ -127,6 +128,7 @@ + diff --git a/src/Orchard.Tests/Tasks/SweepGeneratorTests.cs b/src/Orchard.Tests/Tasks/SweepGeneratorTests.cs new file mode 100644 index 000000000..bfc5b5f27 --- /dev/null +++ b/src/Orchard.Tests/Tasks/SweepGeneratorTests.cs @@ -0,0 +1,43 @@ +using System; +using Autofac.Builder; +using Moq; +using NUnit.Framework; +using Orchard.Tasks; + +namespace Orchard.Tests.Tasks { + [TestFixture] + public class SweepGeneratorTests { + + [Test] + public void DoWorkShouldSendHeartbeatToTaskManager() { + var taskManager = new Mock(); + + var builder = new ContainerBuilder(); + builder.Register(taskManager.Object); + var container = builder.Build(); + + var heartbeatSource = new SweepGenerator(container); + heartbeatSource.DoWork(); + taskManager.Verify(x => x.Sweep(), Times.Once()); + } + + [Test] + public void ActivatedEventShouldStartTimer() { + var taskManager = new Mock(); + + var builder = new ContainerBuilder(); + builder.Register(taskManager.Object); + var container = builder.Build(); + + var heartbeatSource = new SweepGenerator(container) { + Interval = TimeSpan.FromMilliseconds(25) + }; + + taskManager.Verify(x => x.Sweep(), Times.Never()); + heartbeatSource.Activated(); + System.Threading.Thread.Sleep(TimeSpan.FromMilliseconds(80)); + heartbeatSource.Terminating(); + taskManager.Verify(x => x.Sweep(), Times.AtLeastOnce()); + } + } +} diff --git a/src/Orchard.Web/Packages/Orchard.CmsPages/Services/PageScheduler.cs b/src/Orchard.Web/Packages/Orchard.CmsPages/Services/PageScheduler.cs index c0c50dacb..09f6152b9 100644 --- a/src/Orchard.Web/Packages/Orchard.CmsPages/Services/PageScheduler.cs +++ b/src/Orchard.Web/Packages/Orchard.CmsPages/Services/PageScheduler.cs @@ -6,11 +6,11 @@ using JetBrains.Annotations; using Orchard.CmsPages.Models; using Orchard.Data; using Orchard.Services; +using Orchard.Tasks; namespace Orchard.CmsPages.Services { - public interface IPageScheduler : IDependency { + public interface IPageScheduler : IBackgroundTask { void AddPublishTask(PageRevision revision, DateTime moment); - void Sweep(); void ClearTasks(Page page); } diff --git a/src/Orchard/Data/HackSessionLocator.cs b/src/Orchard/Data/HackSessionLocator.cs index e55e69a7a..6db6b116e 100644 --- a/src/Orchard/Data/HackSessionLocator.cs +++ b/src/Orchard/Data/HackSessionLocator.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Reflection; using System.Threading; using System.Web; +using System.Web.Hosting; using FluentNHibernate.Automapping; using FluentNHibernate.Automapping.Alterations; using FluentNHibernate.Cfg; @@ -29,8 +30,9 @@ namespace Orchard.Data { // that would eventually imply the need for configuration against one or more actual sources // and a means to enlist record types from active packages into correct session factory - var database = - SQLiteConfiguration.Standard.UsingFile(HttpContext.Current.Server.MapPath("~/App_Data/hack.db")); + var hackPath = HostingEnvironment.MapPath("~/App_Data/hack.db"); + + var database = SQLiteConfiguration.Standard.UsingFile(hackPath); var recordTypes = _compositionStrategy.GetRecordTypes(); diff --git a/src/Orchard/Environment/DefaultOrchardShell.cs b/src/Orchard/Environment/DefaultOrchardShell.cs index e994aa126..a76988627 100644 --- a/src/Orchard/Environment/DefaultOrchardShell.cs +++ b/src/Orchard/Environment/DefaultOrchardShell.cs @@ -3,9 +3,11 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Web.Mvc; +using Orchard.Logging; using Orchard.Mvc.ModelBinders; using Orchard.Mvc.Routes; using Orchard.Packages; +using Orchard.Tasks; namespace Orchard.Environment { public class DefaultOrchardShell : IOrchardShell { @@ -15,6 +17,7 @@ namespace Orchard.Environment { private readonly IModelBinderPublisher _modelBinderPublisher; private readonly ViewEngineCollection _viewEngines; private readonly IPackageManager _packageManager; + private readonly IEnumerable _events; public DefaultOrchardShell( IEnumerable routeProviders, @@ -22,15 +25,21 @@ namespace Orchard.Environment { IEnumerable modelBinderProviders, IModelBinderPublisher modelBinderPublisher, ViewEngineCollection viewEngines, - IPackageManager packageManager) { + IPackageManager packageManager, + IEnumerable events) { _routeProviders = routeProviders; _routePublisher = routePublisher; _modelBinderProviders = modelBinderProviders; _modelBinderPublisher = modelBinderPublisher; _viewEngines = viewEngines; _packageManager = packageManager; + _events = events; + + Logger = NullLogger.Instance; } + public ILogger Logger { get; set; } + static IEnumerable OrchardLocationFormats() { return new[] { @@ -49,8 +58,8 @@ namespace Orchard.Environment { _routePublisher.Publish(_routeProviders.SelectMany(provider => provider.GetRoutes())); _modelBinderPublisher.Publish(_modelBinderProviders.SelectMany(provider => provider.GetModelBinders())); - - var viewEngine = ViewEngines.Engines.OfType().Single(); + + var viewEngine = _viewEngines.OfType().Single(); viewEngine.AreaViewLocationFormats = OrchardLocationFormats() .Concat(viewEngine.AreaViewLocationFormats) .Distinct() @@ -66,8 +75,16 @@ namespace Orchard.Environment { .Concat(viewEngine.PartialViewLocationFormats) .Distinct() .ToArray(); + + _events.Invoke(x => x.Activated(), Logger); } + + public void Terminate() { + _events.Invoke(x => x.Terminating(), Logger); + } + + private static string ModelsLocationFormat(PackageDescriptor descriptor) { return Path.Combine(Path.Combine(descriptor.Location, descriptor.Name), "Views/Models/{0}.ascx"); } diff --git a/src/Orchard/Environment/IOrchardShell.cs b/src/Orchard/Environment/IOrchardShell.cs index 6ba92a69b..51912e0ea 100644 --- a/src/Orchard/Environment/IOrchardShell.cs +++ b/src/Orchard/Environment/IOrchardShell.cs @@ -1,5 +1,6 @@ namespace Orchard.Environment { public interface IOrchardShell { void Activate(); + void Terminate(); } } \ No newline at end of file diff --git a/src/Orchard/Environment/IOrchardShellEvents.cs b/src/Orchard/Environment/IOrchardShellEvents.cs new file mode 100644 index 000000000..1b1c6ce33 --- /dev/null +++ b/src/Orchard/Environment/IOrchardShellEvents.cs @@ -0,0 +1,6 @@ +namespace Orchard.Environment { + public interface IOrchardShellEvents : IEvents { + void Activated(); + void Terminating(); + } +} diff --git a/src/Orchard/IEvents.cs b/src/Orchard/IEvents.cs new file mode 100644 index 000000000..a8fbfa3cf --- /dev/null +++ b/src/Orchard/IEvents.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using Orchard.Logging; + +namespace Orchard { + public interface IEvents : IDependency { + } + + public static class EventsExtensions { + public static void Invoke(this IEnumerable events, Action dispatch, ILogger logger) where TEvents : IEvents { + foreach (var sink in events) { + try { + dispatch(sink); + } + catch (Exception ex) { + logger.Error(ex, "{2} thrown from {0} by {1}", + typeof(TEvents).Name, + sink.GetType().FullName, + ex.GetType().Name); + } + } + } + } +} diff --git a/src/Orchard/Orchard.csproj b/src/Orchard/Orchard.csproj index d898e5927..c46ddd923 100644 --- a/src/Orchard/Orchard.csproj +++ b/src/Orchard/Orchard.csproj @@ -118,6 +118,8 @@ + + @@ -194,6 +196,9 @@ + + + diff --git a/src/Orchard/Tasks/BackgroundService.cs b/src/Orchard/Tasks/BackgroundService.cs new file mode 100644 index 000000000..e6c03a25e --- /dev/null +++ b/src/Orchard/Tasks/BackgroundService.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using Orchard.Logging; + +namespace Orchard.Tasks { + + public interface IBackgroundService : IDependency { + void Sweep(); + } + + public class BackgroundService : IBackgroundService { + private readonly IEnumerable _tasks; + + public BackgroundService(IEnumerable tasks) { + _tasks = tasks; + Logger = NullLogger.Instance; + } + + public ILogger Logger { get; set; } + + public void Sweep() { + _tasks.Invoke(task => task.Sweep(), Logger); + } + } + +} diff --git a/src/Orchard/Tasks/IBackgroundTask.cs b/src/Orchard/Tasks/IBackgroundTask.cs new file mode 100644 index 000000000..22c3e70d9 --- /dev/null +++ b/src/Orchard/Tasks/IBackgroundTask.cs @@ -0,0 +1,5 @@ +namespace Orchard.Tasks { + public interface IBackgroundTask : IEvents { + void Sweep(); + } +} diff --git a/src/Orchard/Tasks/SweepGenerator.cs b/src/Orchard/Tasks/SweepGenerator.cs new file mode 100644 index 000000000..e403a4a86 --- /dev/null +++ b/src/Orchard/Tasks/SweepGenerator.cs @@ -0,0 +1,97 @@ +using System; +using System.Timers; +using Autofac; +using Autofac.Integration.Web; +using Orchard.Environment; +using Orchard.Logging; + +namespace Orchard.Tasks { + public class SweepGenerator : IOrchardShellEvents { + private readonly IContainer _container; + private Timer _timer; + + public SweepGenerator(IContainer container) { + _container = container; + _timer = new Timer(); + _timer.Elapsed += Elapsed; + Logger = NullLogger.Instance; + } + + public ILogger Logger { get; set; } + + public TimeSpan Interval { + get { return TimeSpan.FromMilliseconds(_timer.Interval); } + set { _timer.Interval = value.Milliseconds; } + } + + public void Activated() { + lock (_timer) { + _timer.Start(); + } + } + + public void Terminating() { + lock (_timer) { + _timer.Stop(); + } + } + + void Elapsed(object sender, ElapsedEventArgs e) { + // current implementation disallows re-entrancy + if (!System.Threading.Monitor.TryEnter(_timer)) + return; + + try { + if (_timer.Enabled) { + DoWork(); + } + } + catch (Exception ex) { + Logger.Warning(ex, "Problem in background tasks"); + } + finally { + System.Threading.Monitor.Exit(_timer); + } + } + + public void DoWork() { + // makes an inner container, similar to the per-request container + + var containerProvider = new ContainerProvider(_container); + try { + var requestContainer = containerProvider.RequestContainer; + + // also inject this instance in case anyone asks for the container provider + requestContainer.Build(builder => builder.Register(containerProvider).As()); + + // resolve the manager and invoke it + var manager = requestContainer.Resolve(); + manager.Sweep(); + } + finally{ + // shut everything down again + containerProvider.DisposeRequestContainer(); + } + } + + class ContainerProvider : IContainerProvider { + public ContainerProvider(IContainer applicationContainer) { + // explicitly create a request container for the life of this object + ApplicationContainer = applicationContainer; + RequestContainer = applicationContainer.CreateInnerContainer(); + } + + public void DisposeRequestContainer() { + var disposeContainer = RequestContainer; + RequestContainer = null; + + if (disposeContainer != null) + disposeContainer.Dispose(); + } + + public IContainer ApplicationContainer { get; private set; } + + public IContainer RequestContainer { get; private set; } + } + } +}