diff --git a/README.md b/README.md index d7a08dc..63364b4 100644 --- a/README.md +++ b/README.md @@ -32,4 +32,8 @@ tasks to run without requiring a bunch of setup or a connection to Azure. This is not a general purpose scheduling framework. There are much better ones out there such as FluentScheduler and Quartz.net. The goal of this project is to handle one task only, manage a recurring task on an interval in the -background for a web app. \ No newline at end of file +background for a web app. + +The needs I have are very simple. I didn't need a high fidelity scheduler. +Maybe later, I'll look to integrate what I've done with one of the others. +But for now, this scratches an itch. \ No newline at end of file diff --git a/src/WebBackgrounder.DemoWeb/SampleJob.cs b/src/WebBackgrounder.DemoWeb/SampleJob.cs index 94343f4..8eb7541 100644 --- a/src/WebBackgrounder.DemoWeb/SampleJob.cs +++ b/src/WebBackgrounder.DemoWeb/SampleJob.cs @@ -1,5 +1,6 @@ using System; using System.Threading; +using System.Threading.Tasks; namespace WebBackgrounder.DemoWeb { @@ -9,9 +10,9 @@ public SampleJob(TimeSpan interval) : base("Sample Job", interval) { } - public override void Execute() + public override Task Execute() { - Thread.Sleep(3000); + return new Task(() => Thread.Sleep(3000)); } } } \ No newline at end of file diff --git a/src/WebBackgrounder.DemoWeb/WebBackgrounder.DemoWeb.csproj b/src/WebBackgrounder.DemoWeb/WebBackgrounder.DemoWeb.csproj index dda3cb4..ac9dbcd 100644 --- a/src/WebBackgrounder.DemoWeb/WebBackgrounder.DemoWeb.csproj +++ b/src/WebBackgrounder.DemoWeb/WebBackgrounder.DemoWeb.csproj @@ -146,7 +146,7 @@ False True - 56071 + 63168 / diff --git a/src/WebBackgrounder.EntityFramework/WorkItemCleanupJob.cs b/src/WebBackgrounder.EntityFramework/WorkItemCleanupJob.cs index 0d7a68a..3689947 100644 --- a/src/WebBackgrounder.EntityFramework/WorkItemCleanupJob.cs +++ b/src/WebBackgrounder.EntityFramework/WorkItemCleanupJob.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Threading.Tasks; using WebBackgrounder.EntityFramework.Entities; namespace WebBackgrounder.EntityFramework @@ -28,24 +29,27 @@ public int MaxWorkItemCount private set; } - public override void Execute() + public override Task Execute() { - var count = _context.WorkItems.Count(); - if (count > MaxWorkItemCount) + return new Task(() => { - var oldest = (from workItem in _context.WorkItems - orderby workItem.Started descending - select workItem).Skip(MaxWorkItemCount).ToList(); - - if (oldest.Count > 0) + var count = _context.WorkItems.Count(); + if (count > MaxWorkItemCount) { - foreach (var workItem in oldest) + var oldest = (from workItem in _context.WorkItems + orderby workItem.Started descending + select workItem).Skip(MaxWorkItemCount).ToList(); + + if (oldest.Count > 0) { - _context.WorkItems.Remove(workItem); + foreach (var workItem in oldest) + { + _context.WorkItems.Remove(workItem); + } + _context.SaveChanges(); } - _context.SaveChanges(); } - } + }); } } } diff --git a/src/WebBackgrounder.UnitTests/JobHostFacts.cs b/src/WebBackgrounder.UnitTests/JobHostFacts.cs index 41565fe..c1d5a83 100644 --- a/src/WebBackgrounder.UnitTests/JobHostFacts.cs +++ b/src/WebBackgrounder.UnitTests/JobHostFacts.cs @@ -13,25 +13,32 @@ public class TheStopMethod public void EnsuresNoWorkIsDone() { var host = new JobHost(); - Action work = () => { throw new InvalidOperationException("Hey, this is supposed to be shut down!"); }; + var task = new Task(() => { throw new InvalidOperationException("Hey, this is supposed to be shut down!"); }); host.Stop(true); - host.DoWork(work); + host.DoWork(task); } [Fact] public void WaitsForTaskToComplete() { var host = new JobHost(); - var workTask = new Task(() => host.DoWork(() => Thread.Sleep(100))); + var workTask = new Task(() => host.DoWork(new Task(() => + { + // Was getting inconsistent results with Thread.Sleep(100) + for (int i = 0; i < 100; i++) + { + Thread.Sleep(1); + } + }))); var beforeStop = DateTime.UtcNow; workTask.Start(); while (workTask.Status != TaskStatus.Running) { Thread.Sleep(1); } - + host.Stop(false); var afterStop = DateTime.UtcNow; diff --git a/src/WebBackgrounder.UnitTests/ScheduleFacts.cs b/src/WebBackgrounder.UnitTests/ScheduleFacts.cs index ea3afab..26f17f4 100644 --- a/src/WebBackgrounder.UnitTests/ScheduleFacts.cs +++ b/src/WebBackgrounder.UnitTests/ScheduleFacts.cs @@ -41,9 +41,11 @@ public void ReturnsTheSpanBetweenNowAndNextRunTime() [Fact] public void ReturnsTheSpanBetweenNowAndNextRunTimeFiguringInLastRun() { + var now = DateTime.UtcNow; + var job = new Mock(); job.Setup(j => j.Interval).Returns(TimeSpan.FromSeconds(30)); - var schedule = new Schedule(job.Object) { LastRunTime = DateTime.UtcNow.AddSeconds(-20)}; + var schedule = new Schedule(job.Object, () => now) { LastRunTime = now.AddSeconds(-20)}; var interval = schedule.GetIntervalToNextRun(); diff --git a/src/WebBackgrounder.UnitTests/SchedulerFacts.cs b/src/WebBackgrounder.UnitTests/SchedulerFacts.cs index 032c80d..b60c92b 100644 --- a/src/WebBackgrounder.UnitTests/SchedulerFacts.cs +++ b/src/WebBackgrounder.UnitTests/SchedulerFacts.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; using Moq; using Xunit; @@ -103,8 +105,9 @@ public WaitJob(int intervalSeconds) : base("Waits", TimeSpan.FromSeconds(interva public int Id { get; private set; } - public override void Execute() + public override Task Execute() { + return new Task(() => Thread.Sleep(1)); } } } diff --git a/src/WebBackgrounder.UnitTests/WorkItemCleanupJobFacts.cs b/src/WebBackgrounder.UnitTests/WorkItemCleanupJobFacts.cs index d6f8faf..e5e72f8 100644 --- a/src/WebBackgrounder.UnitTests/WorkItemCleanupJobFacts.cs +++ b/src/WebBackgrounder.UnitTests/WorkItemCleanupJobFacts.cs @@ -24,8 +24,9 @@ public void DeletesItemsBeyondMaxCount() new WorkItem {Id = 104, Started = DateTime.UtcNow}, }; var job = new WorkItemCleanupJob(2, TimeSpan.FromSeconds(1), context.Object); - - job.Execute(); + var task = job.Execute(); + task.Start(); + task.Wait(); Assert.Equal(2, context.Object.WorkItems.Count()); Assert.Equal(101, context.Object.WorkItems.First().Id); diff --git a/src/WebBackgrounder/IJob.cs b/src/WebBackgrounder/IJob.cs index 912a144..1cd437d 100644 --- a/src/WebBackgrounder/IJob.cs +++ b/src/WebBackgrounder/IJob.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; namespace WebBackgrounder { @@ -9,11 +10,7 @@ public interface IJob /// string Name { get; } - /// - /// Executes the task - /// - /// - void Execute(); + Task Execute(); /// /// Interval in milliseconds. diff --git a/src/WebBackgrounder/IJobCoordinator.cs b/src/WebBackgrounder/IJobCoordinator.cs index cf9bf9e..98384f4 100644 --- a/src/WebBackgrounder/IJobCoordinator.cs +++ b/src/WebBackgrounder/IJobCoordinator.cs @@ -1,11 +1,13 @@ -namespace WebBackgrounder +using System.Threading.Tasks; + +namespace WebBackgrounder { public interface IJobCoordinator { /// - /// Coordinates the work to be done and then does the work if necessary. + /// Coordinates the work to be done and returns a task embodying that work. /// /// - void PerformWork(IJob job); + Task PerformWork(IJob job); } } diff --git a/src/WebBackgrounder/IJobHost.cs b/src/WebBackgrounder/IJobHost.cs index 037e1c6..d274765 100644 --- a/src/WebBackgrounder/IJobHost.cs +++ b/src/WebBackgrounder/IJobHost.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; namespace WebBackgrounder { @@ -8,6 +9,6 @@ namespace WebBackgrounder /// public interface IJobHost { - void DoWork(Action work); + void DoWork(Task work); } } diff --git a/src/WebBackgrounder/Job.cs b/src/WebBackgrounder/Job.cs index ddad302..47aa1bc 100644 --- a/src/WebBackgrounder/Job.cs +++ b/src/WebBackgrounder/Job.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; namespace WebBackgrounder { @@ -22,7 +23,7 @@ public string Name private set; } - public abstract void Execute(); + public abstract Task Execute(); public TimeSpan Interval { diff --git a/src/WebBackgrounder/JobHost.cs b/src/WebBackgrounder/JobHost.cs index fa677eb..df61249 100644 --- a/src/WebBackgrounder/JobHost.cs +++ b/src/WebBackgrounder/JobHost.cs @@ -1,12 +1,12 @@ -using System; +using System.Threading.Tasks; using System.Web.Hosting; namespace WebBackgrounder { public class JobHost : IJobHost, IRegisteredObject { - private readonly object _lock = new object(); - private bool _shuttingDown; + readonly object _lock = new object(); + bool _shuttingDown; public JobHost() { @@ -19,9 +19,10 @@ public void Stop(bool immediate) { _shuttingDown = true; } + HostingEnvironment.UnregisterObject(this); } - public void DoWork(Action work) + public void DoWork(Task work) { lock (_lock) { @@ -29,7 +30,13 @@ public void DoWork(Action work) { return; } - work(); + work.Start(); + // Need to hold the lock until the task completes. + // Later on, we should take advantage of the fact that the work is represented + // by a task. Instead of locking, we could simply have the Stop method cancel + // any pending tasks. + work.Wait(); + } } } diff --git a/src/WebBackgrounder/JobManager.cs b/src/WebBackgrounder/JobManager.cs index 1357952..9a57acd 100644 --- a/src/WebBackgrounder/JobManager.cs +++ b/src/WebBackgrounder/JobManager.cs @@ -71,7 +71,7 @@ void PerformTask() { using (var schedule = _scheduler.Next()) { - _host.DoWork(() => _coordinator.PerformWork(schedule.Job)); + _host.DoWork(_coordinator.PerformWork(schedule.Job)); } } diff --git a/src/WebBackgrounder/SingleServerJobCoordinator.cs b/src/WebBackgrounder/SingleServerJobCoordinator.cs index ab0fc3c..e769228 100644 --- a/src/WebBackgrounder/SingleServerJobCoordinator.cs +++ b/src/WebBackgrounder/SingleServerJobCoordinator.cs @@ -1,10 +1,12 @@ - namespace WebBackgrounder + using System.Threading.Tasks; + +namespace WebBackgrounder { public class SingleServerJobCoordinator : IJobCoordinator { - public void PerformWork(IJob job) + public Task PerformWork(IJob job) { - job.Execute(); + return job.Execute(); } } } diff --git a/src/WebBackgrounder/WebFarmJobCoordinator.cs b/src/WebBackgrounder/WebFarmJobCoordinator.cs index 87e6fc7..62fcf11 100644 --- a/src/WebBackgrounder/WebFarmJobCoordinator.cs +++ b/src/WebBackgrounder/WebFarmJobCoordinator.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; namespace WebBackgrounder { @@ -15,32 +16,37 @@ public WebFarmJobCoordinator(Func repositoryThunk) _repositoryThunk = repositoryThunk; } - public void PerformWork(IJob jobWorker) + public Task PerformWork(IJob job) { // We need a new instance every time we perform work. - var repository = _repositoryThunk(jobWorker.Name); + var repository = _repositoryThunk(job.Name); var unitOfWork = ReserveWork(repository, WorkerId); if (unitOfWork == null) { - return; + return null; } - try - { - jobWorker.Execute(); - unitOfWork.Complete(); - } - catch (Exception exception) + var task = job.Execute(); + task.ContinueWith(c => { - unitOfWork.Fail(exception); - } + if (c.IsFaulted) + { + unitOfWork.Fail(c.Exception.GetBaseException()); + } + else + { + unitOfWork.Complete(); + } + + }); + return task; } public JobUnitOfWork ReserveWork(IWorkItemRepository repository, string workerId) { long? workItemId = null; - + // We do a double check here because this is the first query we run and // a database can't be created inside a transaction scope. if (repository.AnyActiveWorker) diff --git a/src/WebBackgrounderSolution.sln b/src/WebBackgrounderSolution.sln index e17cf7e..8d61d00 100644 --- a/src/WebBackgrounderSolution.sln +++ b/src/WebBackgrounderSolution.sln @@ -9,6 +9,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebBackgrounder.UnitTests", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebBackgrounder.EntityFramework", "WebBackgrounder.EntityFramework\WebBackgrounder.EntityFramework.csproj", "{06D8DE5D-F101-4CD5-B406-8A211216FCE1}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Meta", "Meta", "{3F870BEB-5F40-4CC3-9226-2FBE7A930E81}" + ProjectSection(SolutionItems) = preProject + ..\README.md = ..\README.md + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU