Skip to content

Commit

Permalink
Refactored to task based jobs. Still not taking full advantage of tas…
Browse files Browse the repository at this point in the history
…ks, but this sets the stage for that later.
  • Loading branch information
haacked committed Oct 15, 2011
1 parent 5d199a7 commit b566f4f
Show file tree
Hide file tree
Showing 17 changed files with 98 additions and 55 deletions.
6 changes: 5 additions & 1 deletion README.md
Expand Up @@ -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.
5 changes: 3 additions & 2 deletions src/WebBackgrounder.DemoWeb/SampleJob.cs
@@ -1,5 +1,6 @@
using System;
using System.Threading;
using System.Threading.Tasks;

namespace WebBackgrounder.DemoWeb
{
Expand All @@ -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));
}
}
}
2 changes: 1 addition & 1 deletion src/WebBackgrounder.DemoWeb/WebBackgrounder.DemoWeb.csproj
Expand Up @@ -146,7 +146,7 @@
<WebProjectProperties>
<UseIIS>False</UseIIS>
<AutoAssignPort>True</AutoAssignPort>
<DevelopmentServerPort>56071</DevelopmentServerPort>
<DevelopmentServerPort>63168</DevelopmentServerPort>
<DevelopmentServerVPath>/</DevelopmentServerVPath>
<IISUrl>
</IISUrl>
Expand Down
28 changes: 16 additions & 12 deletions 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
Expand Down Expand Up @@ -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();
}
}
});
}
}
}
15 changes: 11 additions & 4 deletions src/WebBackgrounder.UnitTests/JobHostFacts.cs
Expand Up @@ -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;

Expand Down
4 changes: 3 additions & 1 deletion src/WebBackgrounder.UnitTests/ScheduleFacts.cs
Expand Up @@ -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();

Expand Down
5 changes: 4 additions & 1 deletion 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;

Expand Down Expand Up @@ -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));
}
}
}
Expand Down
5 changes: 3 additions & 2 deletions src/WebBackgrounder.UnitTests/WorkItemCleanupJobFacts.cs
Expand Up @@ -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);
Expand Down
7 changes: 2 additions & 5 deletions src/WebBackgrounder/IJob.cs
@@ -1,4 +1,5 @@
using System;
using System.Threading.Tasks;

namespace WebBackgrounder
{
Expand All @@ -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.
Expand Down
8 changes: 5 additions & 3 deletions 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);
}
}
3 changes: 2 additions & 1 deletion src/WebBackgrounder/IJobHost.cs
@@ -1,4 +1,5 @@
using System;
using System.Threading.Tasks;

namespace WebBackgrounder
{
Expand All @@ -8,6 +9,6 @@ namespace WebBackgrounder
/// </summary>
public interface IJobHost
{
void DoWork(Action work);
void DoWork(Task work);
}
}
3 changes: 2 additions & 1 deletion src/WebBackgrounder/Job.cs
@@ -1,4 +1,5 @@
using System;
using System.Threading.Tasks;

namespace WebBackgrounder
{
Expand All @@ -22,7 +23,7 @@ public string Name
private set;
}

public abstract void Execute();
public abstract Task Execute();

public TimeSpan Interval
{
Expand Down
17 changes: 12 additions & 5 deletions 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()
{
Expand All @@ -19,17 +19,24 @@ public void Stop(bool immediate)
{
_shuttingDown = true;
}
HostingEnvironment.UnregisterObject(this);
}

public void DoWork(Action work)
public void DoWork(Task work)
{
lock (_lock)
{
if (_shuttingDown)
{
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();

}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/WebBackgrounder/JobManager.cs
Expand Up @@ -71,7 +71,7 @@ void PerformTask()
{
using (var schedule = _scheduler.Next())
{
_host.DoWork(() => _coordinator.PerformWork(schedule.Job));
_host.DoWork(_coordinator.PerformWork(schedule.Job));
}
}

Expand Down
8 changes: 5 additions & 3 deletions 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();
}
}
}
30 changes: 18 additions & 12 deletions src/WebBackgrounder/WebFarmJobCoordinator.cs
@@ -1,4 +1,5 @@
using System;
using System.Threading.Tasks;

namespace WebBackgrounder
{
Expand All @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions src/WebBackgrounderSolution.sln
Expand Up @@ -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
Expand Down

0 comments on commit b566f4f

Please sign in to comment.