Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Refactored to task based jobs. Still not taking full advantage of tas…

…ks, but this sets the stage for that later.
  • Loading branch information...
commit b566f4facadcad35c9c821e9432a803b02bb548f 1 parent 5d199a7
@Haacked Haacked authored
View
6 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.
+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.
View
5 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));
}
}
}
View
2  src/WebBackgrounder.DemoWeb/WebBackgrounder.DemoWeb.csproj
@@ -146,7 +146,7 @@
<WebProjectProperties>
<UseIIS>False</UseIIS>
<AutoAssignPort>True</AutoAssignPort>
- <DevelopmentServerPort>56071</DevelopmentServerPort>
+ <DevelopmentServerPort>63168</DevelopmentServerPort>
<DevelopmentServerVPath>/</DevelopmentServerVPath>
<IISUrl>
</IISUrl>
View
28 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();
}
- }
+ });
}
}
}
View
15 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;
View
4 src/WebBackgrounder.UnitTests/ScheduleFacts.cs
@@ -41,9 +41,11 @@ public void ReturnsTheSpanBetweenNowAndNextRunTime()
[Fact]
public void ReturnsTheSpanBetweenNowAndNextRunTimeFiguringInLastRun()
{
+ var now = DateTime.UtcNow;
+
var job = new Mock<IJob>();
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();
View
5 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));
}
}
}
View
5 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);
View
7 src/WebBackgrounder/IJob.cs
@@ -1,4 +1,5 @@
using System;
+using System.Threading.Tasks;
namespace WebBackgrounder
{
@@ -9,11 +10,7 @@ public interface IJob
/// </summary>
string Name { get; }
- /// <summary>
- /// Executes the task
- /// </summary>
- /// <returns></returns>
- void Execute();
+ Task Execute();
/// <summary>
/// Interval in milliseconds.
View
8 src/WebBackgrounder/IJobCoordinator.cs
@@ -1,11 +1,13 @@
-namespace WebBackgrounder
+using System.Threading.Tasks;
+
+namespace WebBackgrounder
{
public interface IJobCoordinator
{
/// <summary>
- /// 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.
/// </summary>
/// <param name="job"></param>
- void PerformWork(IJob job);
+ Task PerformWork(IJob job);
}
}
View
3  src/WebBackgrounder/IJobHost.cs
@@ -1,4 +1,5 @@
using System;
+using System.Threading.Tasks;
namespace WebBackgrounder
{
@@ -8,6 +9,6 @@ namespace WebBackgrounder
/// </summary>
public interface IJobHost
{
- void DoWork(Action work);
+ void DoWork(Task work);
}
}
View
3  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
{
View
17 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();
+
}
}
}
View
2  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));
}
}
View
8 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();
}
}
}
View
30 src/WebBackgrounder/WebFarmJobCoordinator.cs
@@ -1,4 +1,5 @@
using System;
+using System.Threading.Tasks;
namespace WebBackgrounder
{
@@ -15,32 +16,37 @@ public WebFarmJobCoordinator(Func<string, IWorkItemRepository> 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)
View
5 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
Please sign in to comment.
Something went wrong with that request. Please try again.