Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Refactored APIs a bit. Moved some responsibilities around. Need to st…

…art writing more tests.
  • Loading branch information...
commit cf6dcd8c94edaf57b932785b69a0733413e181a7 1 parent 4e813cd
unknown authored
Showing with 255 additions and 241 deletions.
  1. +2 −1  .gitignore
  2. +0 −7 src/WebBackgrounder.EntityFramework/Entities/JobContext.cs
  3. +2 −2 src/WebBackgrounder.EntityFramework/Entities/{Job.cs → JobWorker.cs}
  4. +7 −0 src/WebBackgrounder.EntityFramework/Entities/JobsContext.cs
  5. +0 −82 src/WebBackgrounder.EntityFramework/EntityJobRepository.cs
  6. +78 −0 src/WebBackgrounder.EntityFramework/EntityJobWorkerRepository.cs
  7. +0 −37 src/WebBackgrounder.EntityFramework/IJobRepository.cs
  8. +3 −2 src/WebBackgrounder.EntityFramework/JobStatus.cs
  9. +28 −0 src/WebBackgrounder.EntityFramework/JobUnitOfWork.cs
  10. +4 −4 src/WebBackgrounder.EntityFramework/WebBackgrounder.EntityFramework.csproj
  11. +18 −36 src/WebBackgrounder.EntityFramework/WebFarmJobCoordinator.cs
  12. +15 −12 src/WebBackgrounder.UnitTests/TaskManagerFacts.cs
  13. +1 −1  src/WebBackgrounder/AspNetTaskHost.cs
  14. +22 −0 src/WebBackgrounder/IJob.cs
  15. +5 −3 src/WebBackgrounder/IJobCoordinator.cs
  16. +1 −1  src/WebBackgrounder/{ITaskHost.cs → IJobHost.cs}
  17. +0 −8 src/WebBackgrounder/ITask.cs
  18. +51 −0 src/WebBackgrounder/JobWorkersManager.cs
  19. +0 −42 src/WebBackgrounder/TaskManager.cs
  20. +14 −0 src/WebBackgrounder/TimerExtensions.cs
  21. +4 −3 src/WebBackgrounder/WebBackgrounder.csproj
View
3  .gitignore
@@ -4,4 +4,5 @@ obj
*.suo
App_Data
*.results.xml
-/src/packages
+/src/packages
+_ReSharper.*
View
7 src/WebBackgrounder.EntityFramework/Entities/JobContext.cs
@@ -1,7 +0,0 @@
-using System.Data.Entity;
-
-namespace WebBackgrounder.EntityFramework.Entities {
- public class JobContext : DbContext {
- public DbSet<Job> Jobs { get; set; }
- }
-}
View
4 ...kgrounder.EntityFramework/Entities/Job.cs → ...der.EntityFramework/Entities/JobWorker.cs
@@ -1,9 +1,9 @@
using System;
namespace WebBackgrounder.EntityFramework.Entities {
- public class Job {
+ public class JobWorker {
public int Id { get; set; }
- public string JobName { get; set; }
+ public string Name { get; set; }
public Guid WorkerId { get; set; }
public int Status { get; set; }
}
View
7 src/WebBackgrounder.EntityFramework/Entities/JobsContext.cs
@@ -0,0 +1,7 @@
+using System.Data.Entity;
+
+namespace WebBackgrounder.EntityFramework.Entities {
+ public class JobsContext : DbContext {
+ public DbSet<JobWorker> JobWorkers { get; set; }
+ }
+}
View
82 src/WebBackgrounder.EntityFramework/EntityJobRepository.cs
@@ -1,82 +0,0 @@
-using System;
-using System.Linq;
-using WebBackgrounder.EntityFramework.Entities;
-
-namespace WebBackgrounder.EntityFramework {
- public class EntityJobRepository : IJobRepository {
- readonly JobContext _context;
- public EntityJobRepository(JobContext context) {
- _context = context;
- }
-
- public bool PendingJobsExist(string jobName) {
- var pending = from job in _context.Jobs
- where job.JobName == jobName
- && job.Status != (int)JobStatus.Done
- && job.Status != (int)JobStatus.Ignored
- select job;
- return pending.Any();
- }
-
- public void CreateJobRequest(string jobName, Guid workerId) {
- var job = new Job { JobName = jobName, WorkerId = workerId, Status = (int)JobStatus.Ready };
- _context.Jobs.Add(job);
- _context.SaveChanges();
- }
-
- public Guid GetWorkerIdForJob(string jobName) {
- // Look for the oldest ready job.
- var winner = (from job in _context.Jobs
- where job.JobName == jobName
- && (job.Status == (int)JobStatus.Ready
- || job.Status == (int)JobStatus.Started)
- orderby job.Id ascending
- select job).FirstOrDefault();
-
- if (winner == null) {
- return Guid.Empty;
- }
-
- return winner.WorkerId;
- }
-
- public void CompleteJob(string jobName, Guid workerId) {
- var losers = from job in _context.Jobs
- where job.JobName == jobName
- && job.Status == (int)JobStatus.Ready
- && job.WorkerId != workerId
- select job;
-
- foreach (var loser in losers) {
- loser.Status = (int)JobStatus.Ignored;
- }
- _context.SaveChanges();
-
- var winner = (from job in _context.Jobs
- where job.JobName == jobName
- && job.Status == (int)JobStatus.Ready
- && job.WorkerId == workerId
- select job).FirstOrDefault();
- if (winner == null) {
- throw new InvalidOperationException("No job ready for worker " + workerId);
- }
-
- winner.Status = (int)JobStatus.Done;
- _context.SaveChanges();
- }
-
- public void StartWork(string jobName, Guid workerId) {
- var winner = (from job in _context.Jobs
- where job.JobName == jobName
- && job.Status == (int)JobStatus.Ready
- && job.WorkerId == workerId
- select job).FirstOrDefault();
- if (winner == null) {
- throw new InvalidOperationException("No job ready for worker " + workerId);
- }
-
- winner.Status = (int)JobStatus.Started;
- _context.SaveChanges();
- }
- }
-}
View
78 src/WebBackgrounder.EntityFramework/EntityJobWorkerRepository.cs
@@ -0,0 +1,78 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using WebBackgrounder.EntityFramework.Entities;
+
+namespace WebBackgrounder.EntityFramework {
+ public class EntityJobWorkerRepository {
+ readonly JobsContext _context;
+ readonly IEnumerable<JobWorker> _workers;
+ readonly string _jobName;
+
+ public EntityJobWorkerRepository(JobsContext context, string jobName) {
+ _context = context;
+ _jobName = jobName;
+ _workers = from worker in _context.JobWorkers
+ where worker.Name == jobName
+ && worker.Status < (int)JobStatus.Complete
+ select worker;
+ }
+
+ public bool AnyActiveWorkers() {
+ return _workers.Any();
+ }
+
+ public JobUnitOfWork ReserveWorker(Guid workerId) {
+ var worker = new JobWorker { Name = _jobName, WorkerId = workerId, Status = (int)JobStatus.Ready };
+ _context.JobWorkers.Add(worker);
+ _context.SaveChanges();
+
+ var currentWorker = GetCurrentWorker();
+ return currentWorker.WorkerId == workerId ? new JobUnitOfWork(this, currentWorker) : null;
+ }
+
+ // Retrieves the oldest (aka first inserted) job with the same job name.
+ JobWorker GetCurrentWorker() {
+ // REVIEW: Make absolutely sure this queries the database and doesn't look in some context cache.
+ var currentWorker = (from worker in _context.JobWorkers
+ where worker.Name == _jobName
+ && worker.Status == (int)JobStatus.Ready
+ orderby worker.Id ascending
+ select worker).FirstOrDefault();
+
+ if (currentWorker == null) {
+ throw new InvalidOperationException("Could not find a job to handle this worker, even though we should have just created one.");
+ }
+
+ return currentWorker;
+ }
+
+ public void SetWorkerStarted(JobWorker worker) {
+ worker.Status = (int)JobStatus.Started;
+ _context.SaveChanges();
+ }
+
+ public void SetWorkerComplete(JobWorker worker) {
+ worker.Status = (int)JobStatus.Started;
+ _context.SaveChanges();
+ }
+
+ public void SetWorkerFailed(JobWorker worker) {
+ worker.Status = (int)JobStatus.Started;
+ _context.SaveChanges();
+ }
+
+ public void UpdateIgnoredWorkers(JobWorker currentWorker) {
+ var ignoredWorkers = from worker in _workers
+ where worker.Status == (int)JobStatus.Ready
+ && worker.Name == currentWorker.Name
+ && worker.WorkerId != currentWorker.WorkerId
+ select worker;
+
+ foreach (var loser in ignoredWorkers) {
+ loser.Status = (int)JobStatus.Ignored;
+ }
+ _context.SaveChanges();
+ }
+ }
+}
View
37 src/WebBackgrounder.EntityFramework/IJobRepository.cs
@@ -1,37 +0,0 @@
-using System;
-
-namespace WebBackgrounder.EntityFramework {
- public interface IJobRepository {
- /// <summary>
- /// Returns true if the specified job is currently pending or running.
- /// </summary>
- /// <param name="jobName"></param>
- /// <returns></returns>
- bool PendingJobsExist(string jobName);
-
- /// <summary>
- /// Creates a request from a worker that it's ready to take on a
- /// job. It might not necessarily get it.
- /// </summary>
- void CreateJobRequest(string jobName, Guid workerId);
-
- /// <summary>
- /// Returns the worker Id that gets to do the job.
- /// </summary>
- /// <param name="jobName"></param>
- /// <returns></returns>
- Guid GetWorkerIdForJob(string jobName);
-
- /// <summary>
- /// Indicates that the worker is about to start.
- /// </summary>
- /// <param name="jobName"></param>
- void StartWork(string jobName, Guid workerId);
-
- /// <summary>
- /// Marks the job as complete.
- /// </summary>
- /// <param name="jobName"></param>
- void CompleteJob(string jobName, Guid workerId);
- }
-}
View
5 src/WebBackgrounder.EntityFramework/JobStatus.cs
@@ -1,9 +1,10 @@
namespace WebBackgrounder.EntityFramework {
public enum JobStatus {
- Ignored = 0,
Ready = 1,
Started = 2,
- Done = 3
+ Complete = 3,
+ Failed = 4,
+ Ignored = 5
}
}
View
28 src/WebBackgrounder.EntityFramework/JobUnitOfWork.cs
@@ -0,0 +1,28 @@
+using System;
+using WebBackgrounder.EntityFramework.Entities;
+
+namespace WebBackgrounder.EntityFramework {
+ public class JobUnitOfWork : IDisposable {
+ readonly EntityJobWorkerRepository _repository;
+ readonly JobWorker _currentJob;
+ bool _finished;
+
+ public JobUnitOfWork(EntityJobWorkerRepository repository, JobWorker job) {
+ _currentJob = job;
+ _repository = repository;
+ _repository.SetWorkerStarted(_currentJob);
+ }
+
+ public void Complete() {
+ _repository.UpdateIgnoredWorkers(_currentJob);
+ _repository.SetWorkerComplete(_currentJob);
+ _finished = true;
+ }
+
+ public void Dispose() {
+ if (!_finished) {
+ _repository.SetWorkerFailed(_currentJob);
+ }
+ }
+ }
+}
View
8 src/WebBackgrounder.EntityFramework/WebBackgrounder.EntityFramework.csproj
@@ -48,11 +48,11 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
- <Compile Include="Entities\Job.cs" />
- <Compile Include="Entities\JobContext.cs" />
- <Compile Include="IJobRepository.cs" />
+ <Compile Include="Entities\JobWorker.cs" />
+ <Compile Include="Entities\JobsContext.cs" />
<Compile Include="JobStatus.cs" />
- <Compile Include="EntityJobRepository.cs" />
+ <Compile Include="EntityJobWorkerRepository.cs" />
+ <Compile Include="JobUnitOfWork.cs" />
<Compile Include="WebFarmJobCoordinator.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
View
54 src/WebBackgrounder.EntityFramework/WebFarmJobCoordinator.cs
@@ -1,53 +1,35 @@
using System;
-using System.Transactions;
+using WebBackgrounder.EntityFramework.Entities;
namespace WebBackgrounder.EntityFramework {
/// <summary>
/// Uses the database accessed via EF Code First to coordinate jobs in a web farm.
/// </summary>
public class WebFarmJobCoordinator : IJobCoordinator {
- readonly IJobRepository _repository;
+ readonly JobsContext _context;
+ readonly Guid _workerId = Guid.NewGuid();
- public WebFarmJobCoordinator(IJobRepository repository) {
- _repository = repository;
+ public WebFarmJobCoordinator(JobsContext context) {
+ _context = context;
}
- public bool CanDoWork(string jobName, Guid workerId) {
- bool canDoWork = false;
- using (var transaction = new TransactionScope()) {
- if (!_repository.PendingJobsExist(jobName)) {
- _repository.CreateJobRequest(jobName, workerId);
- canDoWork = _repository.GetWorkerIdForJob(jobName) == workerId;
- }
- transaction.Complete();
+ public void PerformWork(IJob jobWorker) {
+ // We need a new instance every time we perform work.
+ var repository = new EntityJobWorkerRepository(_context, jobWorker.Name);
+
+ // TODO: If the pending job belongs to this worker, we need to deal with that.
+ if (repository.AnyActiveWorkers()) {
+ return;
}
- return canDoWork;
- }
-
- public void Done(string jobName, Guid workerId) {
- _repository.CompleteJob(jobName, workerId);
- }
-
- public IDisposable StartWork(string jobName, Guid workerId) {
- using (var transaction = new TransactionScope()) {
- _repository.StartWork(jobName, workerId);
- }
- return new WorkScope(this, jobName, workerId);
- }
-
- private class WorkScope : IDisposable {
- IJobCoordinator _coordinator;
- string _jobName;
- Guid _workerId;
- public WorkScope(IJobCoordinator coordinator, string jobName, Guid workerId) {
- _coordinator = coordinator;
- _jobName = jobName;
- _workerId = workerId;
+ var unitOfWork = repository.ReserveWorker(_workerId);
+ if (unitOfWork == null) {
+ return;
}
- public void Dispose() {
- _coordinator.Done(_jobName, _workerId);
+ using (unitOfWork) {
+ jobWorker.Execute();
@davidfowl Collaborator

This is async, should be:

jobWorker.Execute().ContinueWith(_ => unitOfWork.Dispose());
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ unitOfWork.Complete();
}
}
}
View
27 src/WebBackgrounder.UnitTests/TaskManagerFacts.cs
@@ -7,26 +7,29 @@ public class TaskManagerFacts {
public class TheRunTaskMethod {
[Fact]
public void DoesNotRunTaskIfHostIsShuttingDown() {
- var task = new Mock<ITask>();
- task.Setup(t => t.Execute()).Throws(new InvalidOperationException("Task should not have been executed because the app was shutting down"));
- var host = new Mock<ITaskHost>();
+ var job = new Mock<IJob>();
+ var host = new Mock<IJobHost>();
host.Setup(h => h.ShuttingDown).Returns(true);
- var taskManager = new TaskManager(null, host.Object, null);
+ var coordinator = new Mock<IJobCoordinator>();
+ coordinator.Setup(c => c.PerformWork(It.IsAny<IJob>())).Throws(new InvalidOperationException("Should not try to do any work"));
+
+ var taskManager = new JobWorkersManager(job.Object, host.Object, coordinator.Object);
- taskManager.RunTask(task.Object);
+ taskManager.RunTask(job.Object);
}
[Fact]
- public void DoesNotRunTaskIfCoordinatorHasNoWork() {
- var task = new Mock<ITask>();
- task.Setup(t => t.Execute()).Throws(new InvalidOperationException("Task should not have been executed because the coordinator said no!"));
- var host = new Mock<ITaskHost>();
+ public void AttemptsToRunTaskIfHostIsNotShuttingDown() {
+ var job = new Mock<IJob>();
+ var host = new Mock<IJobHost>();
host.Setup(h => h.ShuttingDown).Returns(false);
var coordinator = new Mock<IJobCoordinator>();
- coordinator.Setup(c => c.CanDoWork(It.IsAny<string>(), It.IsAny<Guid>())).Returns(false);
- var taskManager = new TaskManager(null, host.Object, coordinator.Object);
+ coordinator.Setup(c => c.PerformWork(job.Object)).Verifiable();
+ var taskManager = new JobWorkersManager(job.Object, host.Object, coordinator.Object);
+
+ taskManager.RunTask(job.Object);
- taskManager.RunTask(task.Object);
+ coordinator.Verify();
}
}
}
View
2  src/WebBackgrounder/AspNetTaskHost.cs
@@ -1,7 +1,7 @@
using System.Web.Hosting;
namespace WebBackgrounder {
- public class AspNetTaskHost : ITaskHost, IRegisteredObject {
+ public class AspNetTaskHost : IJobHost, IRegisteredObject {
public AspNetTaskHost() {
HostingEnvironment.RegisterObject(this);
}
View
22 src/WebBackgrounder/IJob.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Threading.Tasks;
+
+namespace WebBackgrounder {
+ public interface IJob {
+ /// <summary>
+ /// Identifies the type of job. For example, "UpdateStats"
+ /// </summary>
+ string Name { get; }
+
+ /// <summary>
+ /// Executes the task
+ /// </summary>
+ /// <returns></returns>
+ Task Execute();
+
+ /// <summary>
+ /// Interval in milliseconds.
+ /// </summary>
+ TimeSpan Interval { get; }
+ }
+}
View
8 src/WebBackgrounder/IJobCoordinator.cs
@@ -2,8 +2,10 @@
namespace WebBackgrounder {
public interface IJobCoordinator {
- bool CanDoWork(string jobName, Guid workerId);
- IDisposable StartWork(string jobName, Guid workerId);
- void Done(string jobName, Guid workerId);
+ /// <summary>
+ /// Coordinates the work to be done and then does the work if necessary.
+ /// </summary>
+ /// <param name="job"></param>
+ void PerformWork(IJob job);
}
}
View
2  src/WebBackgrounder/ITaskHost.cs → src/WebBackgrounder/IJobHost.cs
@@ -4,7 +4,7 @@ namespace WebBackgrounder {
/// Represents the environment that is hosting the task manager.
/// Typically a web application such as ASP.NET.
/// </summary>
- public interface ITaskHost {
+ public interface IJobHost {
bool ShuttingDown { get; }
}
}
View
8 src/WebBackgrounder/ITask.cs
@@ -1,8 +0,0 @@
-using System.Threading.Tasks;
-
-namespace WebBackgrounder {
- public interface ITask {
- string JobName { get; }
- Task Execute();
- }
-}
View
51 src/WebBackgrounder/JobWorkersManager.cs
@@ -0,0 +1,51 @@
+using System;
+using System.Threading;
+
+namespace WebBackgrounder {
+ public class JobWorkersManager : IDisposable {
+ readonly IJobHost _host;
+ readonly Timer _timer;
+ readonly IJobCoordinator _coordinator;
+ readonly IJob _jobWorker; // We'll make this an enumeration later.
+
+ public JobWorkersManager(IJob jobWorker, IJobHost host, IJobCoordinator coordinator) {
+ _jobWorker = jobWorker;
+ _host = host;
+ _coordinator = coordinator;
+ _timer = new Timer(OnTimerElapsed);
+ }
+
+ public void Start() {
+ _timer.Next(_jobWorker.Interval);
+ }
+
+ public void Stop() {
+ _timer.Dispose();
+ }
+
+ void OnTimerElapsed(object sender) {
+ _timer.Stop();
+ try {
+ // REVIEW: Make sure if this throws, that ELMAH logs it. :)
+ RunTask(_jobWorker);
+ }
+ finally {
+ _timer.Next(_jobWorker.Interval); // Start up again.
+ }
+ }
+
+ public void RunTask(IJob job) {
+ lock (_host) {
+ if (_host.ShuttingDown) {
+ return;
+ }
+ _coordinator.PerformWork(job);
+ }
+ }
+
+ public void Dispose() {
+ Stop();
+ }
+ }
+}
+
View
42 src/WebBackgrounder/TaskManager.cs
@@ -1,42 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Threading;
-
-namespace WebBackgrounder {
- public class TaskManager {
- readonly ITaskHost _host;
- readonly Timer _timer;
- readonly IJobCoordinator _coordinator;
- readonly Guid _workerId = Guid.NewGuid();
- readonly IEnumerable<ITask> _tasks;
-
- public TaskManager(IEnumerable<ITask> tasks, ITaskHost host, IJobCoordinator coordinator) {
- _tasks = tasks;
- _host = host;
- _coordinator = coordinator;
- _timer = new Timer(OnTimerElapsed);
- }
-
- void OnTimerElapsed(object sender) {
- _timer.Change(Timeout.Infinite, Timeout.Infinite); // Stop, in the name of love.
- try {
- foreach (var task in _tasks) {
- RunTask(task);
- }
- }
- finally {
- _timer.Change(10000, Timeout.Infinite); // Start up again.
- }
- }
-
- public void RunTask(ITask task) {
- lock (_host) {
- if (!_host.ShuttingDown && _coordinator.CanDoWork(task.JobName, _workerId)) {
- using (_coordinator.StartWork(task.JobName, _workerId)) {
- task.Execute();
- }
- }
- }
- }
- }
-}
View
14 src/WebBackgrounder/TimerExtensions.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Threading;
+
+namespace WebBackgrounder {
+ public static class TimerExtensions {
+ public static void Stop(this Timer timer) {
+ timer.Change(Timeout.Infinite, Timeout.Infinite);
+ }
+
+ public static void Next(this Timer timer, TimeSpan dueTime) {
+ timer.Change(dueTime, TimeSpan.FromMilliseconds(Timeout.Infinite));
+ }
+ }
+}
View
7 src/WebBackgrounder/WebBackgrounder.csproj
@@ -44,11 +44,12 @@
</ItemGroup>
<ItemGroup>
<Compile Include="IJobCoordinator.cs" />
- <Compile Include="ITask.cs" />
- <Compile Include="ITaskHost.cs" />
+ <Compile Include="IJob.cs" />
+ <Compile Include="IJobHost.cs" />
<Compile Include="AspNetTaskHost.cs" />
- <Compile Include="TaskManager.cs" />
+ <Compile Include="JobWorkersManager.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="TimerExtensions.cs" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$(SolutionDir)\.nuget\NuGet.targets" />
Please sign in to comment.
Something went wrong with that request. Please try again.