[banshee] Add a smart, priority-aware, resource-contention-avoiding job scheduler. Net result is BPM analysis



commit decb115ba2ecfb13d8bc7e1c7924a06d8d1e86f4
Author: Gabriel Burt <gabriel burt gmail com>
Date:   Thu Apr 16 19:15:48 2009 -0500

    Add a smart, priority-aware, resource-contention-avoiding job scheduler. Net result is BPM analysis will pause while importing, etc (BGO #577772). Still need to convert Banshee.Kernel jobs.
    
    2009-04-16  Gabriel Burt  <gabriel burt gmail com>
    
    	* src/Core/Banshee.Core/Banshee.Base/ProductInformation.cs:
    	* src/Core/Banshee.Core/Banshee.Base/Resource.cs: Rename to
    	AssemblyResource to avoid conflicts.
    
    	* src/Core/Banshee.Services/Banshee.Collection/ImportManager.cs:
    	* src/Core/Banshee.Services/Banshee.Collection/RescanPipeline.cs:
    	* src/Core/Banshee.Services/Banshee.Database/BansheeDbFormatMigrator.cs:
    	* src/Core/Banshee.Services/Banshee.Library/ThreadPoolImportSource.cs:
    	* src/Core/Banshee.Services/Banshee.MediaEngine/TranscoderService.cs:
    	* src/Core/Banshee.Services/Banshee.Sources/PrimarySource.cs:
    	* src/Dap/Banshee.Dap.Ipod/Banshee.Dap.Ipod/DatabaseRebuilder.cs:
    	* src/Extensions/Banshee.AudioCd/Banshee.AudioCd/AudioCdRipper.cs:
    	* src/Extensions/Banshee.Bpm/Banshee.Bpm/BpmDetectJob.cs:
    	Set PriorityHints and Resources for Jobs.
    
    	* src/Core/Banshee.Services/Banshee.Metadata/IMetadataLookupJob.cs:
    	* src/Core/Banshee.Services/Banshee.Metadata/MetadataServiceJob.cs: Add
    	Cancel method.
    
    	* src/Core/Banshee.Services/Banshee.ServiceStack/Application.cs: Use the
    	new JobScheduler, checking for DataLossJobs on shutdown.
    
    	* src/Core/Banshee.Services/Banshee.ServiceStack/DbIteratorJob.cs: Rework
    	as subclass of SimpleAsyncJob.
    
    	* src/Core/Banshee.Services/Banshee.ServiceStack/JobScheduler.cs: Simple
    	new class that subclasses JobScheduler and is an IService.
    
    	* src/Core/Banshee.Services/Makefile.am:
    	* src/Core/Banshee.Services/Banshee.ServiceStack/UserJobManager.cs:
    	* src/Core/Banshee.Services/Banshee.ServiceStack/ServiceManager.cs:
    	* src/Core/Banshee.ThickClient/Banshee.Gui.Dialogs/ConfirmShutdownDialog.cs:
    	* src/Core/Banshee.ThickClient/Banshee.Gui.Widgets/TaskStatusIcon.cs:
    	* src/Core/Banshee.ThickClient/Banshee.Gui.Widgets/UserJobTile.cs:
    	* src/Core/Banshee.ThickClient/Banshee.Gui.Widgets/UserJobTileHost.cs:
    	* src/Extensions/Banshee.Bpm/Banshee.Bpm/BpmService.cs:
    	Use JobScheduler instead of UserJobManager.
    
    	* src/Core/Banshee.Services/Banshee.ServiceStack/TestUserJob.cs:
    	* src/Core/Banshee.Services/Banshee.ServiceStack/UserJob.cs: Inherit from
    	Hyena.Job.  Most UserJob code now lives in Hyena.Job
    
    	* src/Extensions/Banshee.CoverArt/Banshee.CoverArt/CoverArtJob.cs: Inherit
    	from DbIteratorJob.
    
    	* src/Extensions/Banshee.CoverArt/Banshee.CoverArt/CoverArtService.cs:
    	If we were cancelled, don't try to save the last_scan time.
    
    	* src/Libraries/Hyena/Makefile.am:
    	* src/Libraries/Hyena/Hyena.Jobs/Job.cs:
    	* src/Libraries/Hyena/Hyena.Jobs/JobExtensions.cs:
    	* src/Libraries/Hyena/Hyena.Jobs/PriorityHints.cs:
    	* src/Libraries/Hyena/Hyena.Jobs/Resource.cs:
    	* src/Libraries/Hyena/Hyena.Jobs/Scheduler.cs:
    	* src/Libraries/Hyena/Hyena.Jobs/SimpleAsyncJob.cs:
    	* src/Libraries/Hyena/Hyena.Jobs/Tests/SchedulerTests.cs: New set of
    	classes to implement smart scheduling of jobs, prioritizing jobs and
    	avoiding resource contention.
    
    	* src/Libraries/Hyena/Hyena/Timer.cs: Add params/format ctor.
---
 ChangeLog                                          |   67 +++++
 .../Banshee.Base/ProductInformation.cs             |    6 +-
 src/Core/Banshee.Core/Banshee.Base/Resource.cs     |    2 +-
 .../Banshee.Collection/ImportManager.cs            |    3 +
 .../Banshee.Collection/RescanPipeline.cs           |    3 +
 .../Banshee.Database/BansheeDbFormatMigrator.cs    |    5 +-
 .../Banshee.Library/ThreadPoolImportSource.cs      |    6 +-
 .../Banshee.MediaEngine/TranscoderService.cs       |    4 +
 .../Banshee.Metadata/IMetadataLookupJob.cs         |    1 +
 .../Banshee.Metadata/MetadataServiceJob.cs         |   33 ++-
 .../Banshee.ServiceStack/Application.cs            |    2 +
 .../Banshee.ServiceStack/DbIteratorJob.cs          |   51 ++--
 .../Banshee.ServiceStack/JobScheduler.cs           |   41 +++
 .../Banshee.ServiceStack/ServiceManager.cs         |    7 +-
 .../Banshee.ServiceStack/TestUserJob.cs            |    3 +-
 .../Banshee.ServiceStack/UserJob.cs                |  151 +---------
 .../Banshee.ServiceStack/UserJobManager.cs         |   99 ------
 .../Banshee.Sources/PrimarySource.cs               |   11 +-
 src/Core/Banshee.Services/Makefile.am              |    2 +-
 .../Banshee.Gui.Dialogs/ConfirmShutdownDialog.cs   |   32 ++-
 .../Banshee.Gui.Widgets/TaskStatusIcon.cs          |   20 +-
 .../Banshee.Gui.Widgets/UserJobTile.cs             |   10 +-
 .../Banshee.Gui.Widgets/UserJobTileHost.cs         |   33 +-
 .../Banshee.Dap.Ipod/DatabaseRebuilder.cs          |    3 +
 .../Banshee.AudioCd/AudioCdRipper.cs               |    4 +
 .../Banshee.Bpm/Banshee.Bpm/BpmDetectJob.cs        |   45 +++-
 .../Banshee.Bpm/Banshee.Bpm/BpmService.cs          |    3 +-
 .../Banshee.CoverArt/CoverArtJob.cs                |  146 ++++------
 .../Banshee.CoverArt/CoverArtService.cs            |    5 +-
 src/Libraries/Hyena/Hyena.Jobs/Job.cs              |  312 ++++++++++++++++++++
 src/Libraries/Hyena/Hyena.Jobs/JobExtensions.cs    |   69 +++++
 src/Libraries/Hyena/Hyena.Jobs/PriorityHints.cs    |   41 +++
 src/Libraries/Hyena/Hyena.Jobs/Resource.cs         |   43 +++
 src/Libraries/Hyena/Hyena.Jobs/Scheduler.cs        |  230 ++++++++++++++
 src/Libraries/Hyena/Hyena.Jobs/SimpleAsyncJob.cs   |   79 +++++
 .../Hyena/Hyena.Jobs/Tests/SchedulerTests.cs       |  203 +++++++++++++
 src/Libraries/Hyena/Hyena/Timer.cs                 |    4 +
 src/Libraries/Hyena/Makefile.am                    |    7 +
 38 files changed, 1367 insertions(+), 419 deletions(-)

diff --git a/ChangeLog b/ChangeLog
index d77b497..cebf856 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,72 @@
 2009-04-16  Gabriel Burt  <gabriel burt gmail com>
 
+	Add a smart, priority-aware, resource-contention-avoiding job scheduler
+	Net result is BPM analysis will pause while importing, etc (BGO #577772).
+	Still need to convert Banshee.Kernel jobs.
+
+	* src/Core/Banshee.Core/Banshee.Base/ProductInformation.cs:
+	* src/Core/Banshee.Core/Banshee.Base/Resource.cs: Rename to
+	AssemblyResource to avoid conflicts.
+
+	* src/Core/Banshee.Services/Banshee.Collection/ImportManager.cs:
+	* src/Core/Banshee.Services/Banshee.Collection/RescanPipeline.cs:
+	* src/Core/Banshee.Services/Banshee.Database/BansheeDbFormatMigrator.cs:
+	* src/Core/Banshee.Services/Banshee.Library/ThreadPoolImportSource.cs:
+	* src/Core/Banshee.Services/Banshee.MediaEngine/TranscoderService.cs:
+	* src/Core/Banshee.Services/Banshee.Sources/PrimarySource.cs:
+	* src/Dap/Banshee.Dap.Ipod/Banshee.Dap.Ipod/DatabaseRebuilder.cs:
+	* src/Extensions/Banshee.AudioCd/Banshee.AudioCd/AudioCdRipper.cs:
+	* src/Extensions/Banshee.Bpm/Banshee.Bpm/BpmDetectJob.cs:
+	Set PriorityHints and Resources for Jobs.
+
+	* src/Core/Banshee.Services/Banshee.Metadata/IMetadataLookupJob.cs:
+	* src/Core/Banshee.Services/Banshee.Metadata/MetadataServiceJob.cs: Add
+	Cancel method.
+
+	* src/Core/Banshee.Services/Banshee.ServiceStack/Application.cs: Use the
+	new JobScheduler, checking for DataLossJobs on shutdown.
+
+	* src/Core/Banshee.Services/Banshee.ServiceStack/DbIteratorJob.cs: Rework
+	as subclass of SimpleAsyncJob.
+
+	* src/Core/Banshee.Services/Banshee.ServiceStack/JobScheduler.cs: Simple
+	new class that subclasses JobScheduler and is an IService.
+
+	* src/Core/Banshee.Services/Makefile.am:
+	* src/Core/Banshee.Services/Banshee.ServiceStack/UserJobManager.cs:
+	* src/Core/Banshee.Services/Banshee.ServiceStack/ServiceManager.cs:
+	* src/Core/Banshee.ThickClient/Banshee.Gui.Dialogs/ConfirmShutdownDialog.cs:
+	* src/Core/Banshee.ThickClient/Banshee.Gui.Widgets/TaskStatusIcon.cs:
+	* src/Core/Banshee.ThickClient/Banshee.Gui.Widgets/UserJobTile.cs:
+	* src/Core/Banshee.ThickClient/Banshee.Gui.Widgets/UserJobTileHost.cs:
+	* src/Extensions/Banshee.Bpm/Banshee.Bpm/BpmService.cs:
+	Use JobScheduler instead of UserJobManager.
+
+	* src/Core/Banshee.Services/Banshee.ServiceStack/TestUserJob.cs:
+	* src/Core/Banshee.Services/Banshee.ServiceStack/UserJob.cs: Inherit from
+	Hyena.Job.  Most UserJob code now lives in Hyena.Job
+
+	* src/Extensions/Banshee.CoverArt/Banshee.CoverArt/CoverArtJob.cs: Inherit
+	from DbIteratorJob.
+
+	* src/Extensions/Banshee.CoverArt/Banshee.CoverArt/CoverArtService.cs:
+	If we were cancelled, don't try to save the last_scan time.
+
+	* src/Libraries/Hyena/Makefile.am:
+	* src/Libraries/Hyena/Hyena.Jobs/Job.cs:
+	* src/Libraries/Hyena/Hyena.Jobs/JobExtensions.cs:
+	* src/Libraries/Hyena/Hyena.Jobs/PriorityHints.cs:
+	* src/Libraries/Hyena/Hyena.Jobs/Resource.cs:
+	* src/Libraries/Hyena/Hyena.Jobs/Scheduler.cs:
+	* src/Libraries/Hyena/Hyena.Jobs/SimpleAsyncJob.cs:
+	* src/Libraries/Hyena/Hyena.Jobs/Tests/SchedulerTests.cs: New set of
+	classes to implement smart scheduling of jobs, prioritizing jobs and
+	avoiding resource contention.
+
+	* src/Libraries/Hyena/Hyena/Timer.cs: Add params/format ctor.
+
+2009-04-16  Gabriel Burt  <gabriel burt gmail com>
+
 	This patch adds separate library folder preferences for Music Library,
 	Video Library, and Podcasts (BGO #404827).  They are all configurable via
 	the Preferences dialog.  It also changes the format used to store URIs in
diff --git a/src/Core/Banshee.Core/Banshee.Base/ProductInformation.cs b/src/Core/Banshee.Core/Banshee.Base/ProductInformation.cs
index ffc0293..81445bb 100644
--- a/src/Core/Banshee.Core/Banshee.Base/ProductInformation.cs
+++ b/src/Core/Banshee.Core/Banshee.Base/ProductInformation.cs
@@ -55,7 +55,7 @@ namespace Banshee.Base
             List<string> contributors_list = new List<string> ();
         
             XmlDocument doc = new XmlDocument ();
-            doc.LoadXml (Resource.GetFileContents ("contributors.xml"));
+            doc.LoadXml (AssemblyResource.GetFileContents ("contributors.xml"));
         
             foreach (XmlNode node in doc.DocumentElement.ChildNodes) {
                 if (node.FirstChild == null || node.FirstChild.Value == null) {
@@ -89,7 +89,7 @@ namespace Banshee.Base
         private static void LoadTranslators ()
         {
             XmlDocument doc = new XmlDocument ();
-            doc.LoadXml (Resource.GetFileContents ("translators.xml"));
+            doc.LoadXml (AssemblyResource.GetFileContents ("translators.xml"));
         
             foreach (XmlNode node in doc.DocumentElement.ChildNodes) {
                 if (node.Name != "language") {
@@ -133,7 +133,7 @@ namespace Banshee.Base
         }
         
         public static string License {
-            get { return Resource.GetFileContents ("COPYING"); }
+            get { return AssemblyResource.GetFileContents ("COPYING"); }
         }
     }
     
diff --git a/src/Core/Banshee.Core/Banshee.Base/Resource.cs b/src/Core/Banshee.Core/Banshee.Base/Resource.cs
index f01c9dd..6de004a 100644
--- a/src/Core/Banshee.Core/Banshee.Base/Resource.cs
+++ b/src/Core/Banshee.Core/Banshee.Base/Resource.cs
@@ -32,7 +32,7 @@ using System.Reflection;
 
 namespace Banshee.Base
 {
-    public static class Resource
+    public static class AssemblyResource
     {
         public static string GetFileContents (string name)
         {
diff --git a/src/Core/Banshee.Services/Banshee.Collection/ImportManager.cs b/src/Core/Banshee.Services/Banshee.Collection/ImportManager.cs
index bbc767e..d759a5a 100644
--- a/src/Core/Banshee.Services/Banshee.Collection/ImportManager.cs
+++ b/src/Core/Banshee.Services/Banshee.Collection/ImportManager.cs
@@ -31,6 +31,7 @@ using System.Globalization;
 using Mono.Unix;
 
 using Hyena;
+using Hyena.Jobs;
 using Hyena.Collections;
 
 using Banshee.IO;
@@ -134,6 +135,8 @@ namespace Banshee.Collection
                 timer_id = Log.DebugTimerStart ();
                 
                 user_job = new UserJob (Title, Catalog.GetString ("Scanning for media"));
+                user_job.SetResources (Resource.Cpu, Resource.Disk, Resource.Database);
+                user_job.PriorityHints = PriorityHints.SpeedSensitive | PriorityHints.DataLossIfStopped;
                 user_job.IconNames = new string [] { "system-search", "gtk-find" };
                 user_job.CancelMessage = CancelMessage;
                 user_job.CanCancel = true;
diff --git a/src/Core/Banshee.Services/Banshee.Collection/RescanPipeline.cs b/src/Core/Banshee.Services/Banshee.Collection/RescanPipeline.cs
index 66aa9c8..6cc1b87 100644
--- a/src/Core/Banshee.Services/Banshee.Collection/RescanPipeline.cs
+++ b/src/Core/Banshee.Services/Banshee.Collection/RescanPipeline.cs
@@ -31,6 +31,7 @@ using System.Data;
 
 using Mono.Unix;
 
+using Hyena.Jobs;
 using Hyena.Collections;
 using Hyena.Data.Sqlite;
 
@@ -75,6 +76,8 @@ namespace Banshee.Collection
         private void BuildJob ()
         {
             job = new BatchUserJob (Catalog.GetString ("Rescanning {0} of {1}"), "system-search", "gtk-find");
+            job.SetResources (Resource.Cpu, Resource.Disk, Resource.Database);
+            job.PriorityHints = PriorityHints.SpeedSensitive;
             job.CanCancel = true;
             job.CancelRequested += delegate { cancelled = true; Cancel (); };
             track_sync.ProcessedItem += delegate {
diff --git a/src/Core/Banshee.Services/Banshee.Database/BansheeDbFormatMigrator.cs b/src/Core/Banshee.Services/Banshee.Database/BansheeDbFormatMigrator.cs
index f3dc17f..ad6fd25 100644
--- a/src/Core/Banshee.Services/Banshee.Database/BansheeDbFormatMigrator.cs
+++ b/src/Core/Banshee.Services/Banshee.Database/BansheeDbFormatMigrator.cs
@@ -35,6 +35,7 @@ using System.Threading;
 using Mono.Unix;
 
 using Hyena;
+using Hyena.Jobs;
 using Hyena.Data.Sqlite;
 using Timer=Hyena.Timer;
 
@@ -1078,7 +1079,7 @@ namespace Banshee.Database
 
         private void OnServiceStarted (ServiceStartedArgs args)
         {
-            if (args.Service is UserJobManager) {
+            if (args.Service is JobScheduler) {
                 ServiceManager.ServiceStarted -= OnServiceStarted;
                 if (ServiceManager.SourceManager.MusicLibrary != null) {
                     RefreshMetadataDelayed ();
@@ -1130,6 +1131,8 @@ namespace Banshee.Database
             }
 
             UserJob job = new UserJob (Catalog.GetString ("Refreshing Metadata"));
+            job.SetResources (Resource.Cpu, Resource.Disk, Resource.Database);
+            job.PriorityHints = PriorityHints.SpeedSensitive;
             job.Status = Catalog.GetString ("Scanning...");
             job.IconNames = new string [] { "system-search", "gtk-find" };
             job.Register ();
diff --git a/src/Core/Banshee.Services/Banshee.Library/ThreadPoolImportSource.cs b/src/Core/Banshee.Services/Banshee.Library/ThreadPoolImportSource.cs
index c7ce245..e6e6e1d 100644
--- a/src/Core/Banshee.Services/Banshee.Library/ThreadPoolImportSource.cs
+++ b/src/Core/Banshee.Services/Banshee.Library/ThreadPoolImportSource.cs
@@ -30,9 +30,11 @@ using System;
 using System.IO;
 using System.Threading;
 
-using Hyena;
 using Mono.Unix;
 
+using Hyena;
+using Hyena.Jobs;
+
 using Banshee.ServiceStack;
 using Banshee.Sources;
 
@@ -52,6 +54,8 @@ namespace Banshee.Library
                 }
                 
                 user_job = new UserJob (UserJobTitle, UserJobTitle, Catalog.GetString ("Importing Songs"));
+                user_job.SetResources (Resource.Cpu, Resource.Disk, Resource.Database);
+                user_job.PriorityHints = PriorityHints.SpeedSensitive | PriorityHints.DataLossIfStopped;
                 user_job.IconNames = IconNames;
                 user_job.CancelMessage = CancelMessage;
                 user_job.CanCancel = CanCancel;
diff --git a/src/Core/Banshee.Services/Banshee.MediaEngine/TranscoderService.cs b/src/Core/Banshee.Services/Banshee.MediaEngine/TranscoderService.cs
index 880ed54..77db232 100644
--- a/src/Core/Banshee.Services/Banshee.MediaEngine/TranscoderService.cs
+++ b/src/Core/Banshee.Services/Banshee.MediaEngine/TranscoderService.cs
@@ -33,6 +33,8 @@ using System.Collections.Generic;
 using Mono.Unix;
 using Mono.Addins;
 
+using Hyena.Jobs;
+
 using Banshee.Base;
 using Banshee.ServiceStack;
 using Banshee.Collection;
@@ -130,6 +132,8 @@ namespace Banshee.MediaEngine
             get {
                 if (user_job == null) {
                     user_job = new BatchUserJob (Catalog.GetString("Converting {0} of {1}"), Catalog.GetString("Initializing"), "encode");
+                    user_job.SetResources (Resource.Cpu);
+                    user_job.PriorityHints = PriorityHints.SpeedSensitive;
                     user_job.CancelMessage = Catalog.GetString ("Files are currently being converted to another format. Would you like to stop this?");
                     user_job.CanCancel = true;
                     user_job.DelayShow = true;
diff --git a/src/Core/Banshee.Services/Banshee.Metadata/IMetadataLookupJob.cs b/src/Core/Banshee.Services/Banshee.Metadata/IMetadataLookupJob.cs
index 5cee133..5ac2303 100644
--- a/src/Core/Banshee.Services/Banshee.Metadata/IMetadataLookupJob.cs
+++ b/src/Core/Banshee.Services/Banshee.Metadata/IMetadataLookupJob.cs
@@ -40,5 +40,6 @@ namespace Banshee.Metadata
     {
         IBasicTrackInfo Track { get; }
         IList<StreamTag> ResultTags { get; }
+        void Cancel ();
     }
 }
diff --git a/src/Core/Banshee.Services/Banshee.Metadata/MetadataServiceJob.cs b/src/Core/Banshee.Services/Banshee.Metadata/MetadataServiceJob.cs
index 4096b25..97308c1 100644
--- a/src/Core/Banshee.Services/Banshee.Metadata/MetadataServiceJob.cs
+++ b/src/Core/Banshee.Services/Banshee.Metadata/MetadataServiceJob.cs
@@ -43,8 +43,10 @@ namespace Banshee.Metadata
     public class MetadataServiceJob : IMetadataLookupJob
     {
         private MetadataService service;
+        private bool cancelled;
         private IBasicTrackInfo track;
         private List<StreamTag> tags = new List<StreamTag>();
+        private IMetadataLookupJob current_job;
         
         protected bool InternetConnected {
             get { return ServiceManager.Get<Network> ().Connected; }
@@ -59,17 +61,42 @@ namespace Banshee.Metadata
             this.service = service;
             this.track = track;
         }
+
+        public virtual void Cancel ()
+        {
+            cancelled = true;
+            lock (this) {
+                if (current_job != null) {
+                    current_job.Cancel ();
+                }
+            }
+        }
     
         public virtual void Run()
         {
             foreach(IMetadataProvider provider in service.Providers) {
+                if (cancelled)
+                    break;;
+
                 try {
-                    IMetadataLookupJob job = provider.CreateJob(track);
-                    job.Run();
+                    lock (this) {
+                        current_job = provider.CreateJob(track);
+                    }
+
+                    current_job.Run();
+
+                    if (cancelled)
+                        break;;
                     
-                    foreach(StreamTag tag in job.ResultTags) {
+                    foreach(StreamTag tag in current_job.ResultTags) {
                         AddTag(tag);
                     }
+
+                    lock (this) {
+                        current_job = null;
+                    }
+                } catch (System.Threading.ThreadAbortException) {
+                    throw;
                 } catch(Exception e) {
                    Hyena.Log.Exception (e);
                 }
diff --git a/src/Core/Banshee.Services/Banshee.ServiceStack/Application.cs b/src/Core/Banshee.Services/Banshee.ServiceStack/Application.cs
index defb002..67dfcd0 100644
--- a/src/Core/Banshee.Services/Banshee.ServiceStack/Application.cs
+++ b/src/Core/Banshee.Services/Banshee.ServiceStack/Application.cs
@@ -104,6 +104,7 @@ namespace Banshee.ServiceStack
         {
             shutting_down = true;
             if (Banshee.Kernel.Scheduler.IsScheduled (typeof (Banshee.Kernel.IInstanceCriticalJob)) ||
+                ServiceManager.JobScheduler.HasAnyDataLossJobs ||
                 Banshee.Kernel.Scheduler.CurrentJob is Banshee.Kernel.IInstanceCriticalJob) {
                 if (shutdown_prompt_handler != null && !shutdown_prompt_handler ()) {
                     shutting_down = false;
@@ -198,6 +199,7 @@ namespace Banshee.ServiceStack
         
         private static void Dispose ()
         {
+            ServiceManager.JobScheduler.CancelAll (true);
             ServiceManager.Shutdown ();
             
             lock (running_clients) {
diff --git a/src/Core/Banshee.Services/Banshee.ServiceStack/DbIteratorJob.cs b/src/Core/Banshee.Services/Banshee.ServiceStack/DbIteratorJob.cs
index d0fb1e2..6ce6820 100644
--- a/src/Core/Banshee.Services/Banshee.ServiceStack/DbIteratorJob.cs
+++ b/src/Core/Banshee.Services/Banshee.ServiceStack/DbIteratorJob.cs
@@ -35,13 +35,13 @@ using Mono.Unix;
 using Mono.Addins;
 
 using Hyena;
+using Hyena.Jobs;
 using Hyena.Data.Sqlite;
 
 using Banshee.Base;
 using Banshee.Collection;
 using Banshee.Collection.Database;
 using Banshee.Sources;
-using Banshee.Kernel;
 using Banshee.Metadata;
 using Banshee.MediaEngine;
 using Banshee.ServiceStack;
@@ -49,10 +49,8 @@ using Banshee.Library;
 
 namespace Banshee.ServiceStack
 {
-    public abstract class DbIteratorJob : UserJob
+    public abstract class DbIteratorJob : SimpleAsyncJob
     {
-        //private Thread thread;
-
         private HyenaSqliteCommand count_command;
         private HyenaSqliteCommand select_command;
         private int current_count;
@@ -65,49 +63,64 @@ namespace Banshee.ServiceStack
             set { select_command = value; }
         }
 
-        public DbIteratorJob (string name) : base (name)
+        public DbIteratorJob (string title)
+        {
+            Title = title;
+            CancelRequested += OnCancelled;
+        }
+
+        public void Register ()
+        {
+            ServiceManager.JobScheduler.Add (this);
+        }
+
+        private void OnCancelled (object o, EventArgs args)
         {
+            OnCancelled ();
         }
 
-        public void RunAsync ()
+        protected virtual void OnCancelled ()
         {
-            //thread = ThreadAssist.Spawn (Run);
-            ThreadAssist.Spawn (Run);
-            //thread.Name = Title;
         }
 
-        public void Run ()
+        protected override void Run ()
         {
-            int total = ServiceManager.DbConnection.Query<int> (count_command);
-            if (total > 0) {
+            if (ServiceManager.DbConnection.Query<int> (count_command) > 0) {
                 Init ();
-                Iterate ();
+                while (!IsFinished && Iterate ()) {}
             }
+
+            Cleanup ();
         }
 
-        protected void Iterate ()
+        protected bool Iterate ()
         {
             if (IsCancelRequested) {
-                Hyena.Log.DebugFormat ("{0} cancelled", Title);
-                Cleanup ();
-                return;
+                return false;
             }
 
+            YieldToScheduler ();
+
             int total = current_count + ServiceManager.DbConnection.Query<int> (count_command);
             try {
                 using (HyenaDataReader reader = new HyenaDataReader (ServiceManager.DbConnection.Query (select_command))) {
                     if (reader.Read ()) {
                         IterateCore (reader);
                     } else {
-                        Cleanup ();
+                        return false;
                     }
                 }
+            } catch (System.Threading.ThreadAbortException) {
+                Cleanup ();
+                throw;
             } catch (Exception e) {
                 Log.Exception (e);
             } finally {
                 Progress = (double) current_count / (double) total;
                 current_count++;
             }
+
+            return true;
         }
 
         protected virtual void Init ()
@@ -116,7 +129,7 @@ namespace Banshee.ServiceStack
 
         protected virtual void Cleanup ()
         {
-            Finish ();
+            OnFinished ();
         }
    
         protected abstract void IterateCore (HyenaDataReader reader);
diff --git a/src/Core/Banshee.Services/Banshee.ServiceStack/JobScheduler.cs b/src/Core/Banshee.Services/Banshee.ServiceStack/JobScheduler.cs
new file mode 100644
index 0000000..c043c03
--- /dev/null
+++ b/src/Core/Banshee.Services/Banshee.ServiceStack/JobScheduler.cs
@@ -0,0 +1,41 @@
+//
+// JobScheduler.cs
+//
+// Author:
+//   Gabriel Burt <gburt novell com>
+//
+// Copyright (C) 2009 Novell, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+
+using Hyena.Jobs;
+
+namespace Banshee.ServiceStack
+{
+    public class JobScheduler : Scheduler, IRequiredService
+    {
+        string IService.ServiceName {
+            get { return "JobScheduler"; }
+        }
+    }
+}
diff --git a/src/Core/Banshee.Services/Banshee.ServiceStack/ServiceManager.cs b/src/Core/Banshee.Services/Banshee.ServiceStack/ServiceManager.cs
index b406e8f..0dcdd24 100644
--- a/src/Core/Banshee.Services/Banshee.ServiceStack/ServiceManager.cs
+++ b/src/Core/Banshee.Services/Banshee.ServiceStack/ServiceManager.cs
@@ -33,6 +33,7 @@ using System.Collections.Generic;
 using Mono.Addins;
 
 using Hyena;
+
 using Banshee.Base;
 using Banshee.MediaProfiles;
 using Banshee.Sources;
@@ -98,7 +99,7 @@ namespace Banshee.ServiceStack
             RegisterService<PlaybackControllerService> ();
             RegisterService<ImportSourceManager> ();
             RegisterService<LibraryImportManager> ();
-            RegisterService<UserJobManager> ();
+            RegisterService<JobScheduler> ();
             RegisterService<Banshee.Hardware.HardwareManager> ();
             RegisterService<Banshee.Collection.Indexer.CollectionIndexerService> ();
         }
@@ -392,6 +393,10 @@ namespace Banshee.ServiceStack
         public static SourceManager SourceManager {
             get { return (SourceManager)Get ("SourceManager"); }
         }
+
+        public static JobScheduler JobScheduler {
+            get { return (JobScheduler)Get ("JobScheduler"); }
+        }
         
         public static PlayerEngineService PlayerEngine {
             get { return (PlayerEngineService)Get ("PlayerEngine"); }
diff --git a/src/Core/Banshee.Services/Banshee.ServiceStack/TestUserJob.cs b/src/Core/Banshee.Services/Banshee.ServiceStack/TestUserJob.cs
index 263ae0d..df98fab 100644
--- a/src/Core/Banshee.Services/Banshee.ServiceStack/TestUserJob.cs
+++ b/src/Core/Banshee.Services/Banshee.ServiceStack/TestUserJob.cs
@@ -52,6 +52,7 @@ namespace Banshee.ServiceStack
         
         public TestUserJob () : base ("UserJob Test Job", "Waiting for 7.5 seconds...")
         {
+            CancelRequested += OnCancelRequested;
             DelayShow = true;
             Register ();
             
@@ -120,7 +121,7 @@ namespace Banshee.ServiceStack
             });
         }
         
-        protected override void OnCancelRequested ()
+        private void OnCancelRequested (object o, EventArgs args)
         {
             if (initial_timeout_id > 0) {
                 Application.IdleTimeoutRemove (initial_timeout_id);
diff --git a/src/Core/Banshee.Services/Banshee.ServiceStack/UserJob.cs b/src/Core/Banshee.Services/Banshee.ServiceStack/UserJob.cs
index 5d83d37..0e0320e 100644
--- a/src/Core/Banshee.Services/Banshee.ServiceStack/UserJob.cs
+++ b/src/Core/Banshee.Services/Banshee.ServiceStack/UserJob.cs
@@ -29,34 +29,18 @@
 using System;
 using System.Threading;
 
+using Hyena.Jobs;
 using Hyena.Data;
 
 namespace Banshee.ServiceStack
 {
-    public class UserJob : IUserJob
+    public class UserJob : Job
     {
-        private string title;
-        private string status;
-        private double progress;
-        private string [] icon_names;
-        private string cancel_message;
-        private bool can_cancel;
-        private bool is_cancel_requested;
-        private bool is_finished;
-        private bool delay_show;
-        private bool is_background;
-        
-        private int update_freeze_ref;
-        
-        public event EventHandler Finished;
-        public event EventHandler Updated;
-        public event EventHandler CancelRequested;
-        
-        public UserJob (string title, string status) : this (title, status, null)
+        public UserJob (string title) : this (title, null, null)
         {
         }
 
-        public UserJob (string title) : this (title, null, null)
+        public UserJob (string title, string status) : this (title, status, null)
         {
         }
 
@@ -68,134 +52,15 @@ namespace Banshee.ServiceStack
             IconNames = iconNames;
             ThawUpdate (true);
         }
-        
+
         public void Register ()
         {
-            if (ServiceManager.Contains<UserJobManager> ()) {
-                ServiceManager.Get<UserJobManager> ().Register (this);
-            }
-        }
-        
-        public void Cancel ()
-        {
-            OnCancelRequested ();
+            ServiceManager.JobScheduler.Add (this);
         }
-        
+
         public void Finish ()
         {
-            if (!is_finished) {
-                is_finished = true;
-                OnFinished ();
-            }
-        }
-        
-        protected void FreezeUpdate ()
-        {
-            Interlocked.Increment (ref update_freeze_ref);
-        }
-        
-        protected void ThawUpdate (bool raiseUpdate)
-        {
-            Interlocked.Decrement (ref update_freeze_ref);
-            if (raiseUpdate) {
-                OnUpdated ();
-            }
-        }
-        
-        protected virtual void OnFinished ()
-        {
-            EventHandler handler = Finished;
-            if (handler != null) {
-                handler (this, EventArgs.Empty);
-            }
-        }
-        
-        protected virtual void OnUpdated ()
-        {
-            if (update_freeze_ref != 0) {
-                return;
-            }
-            
-            EventHandler handler = Updated;
-            if (handler != null) {
-                handler (this, EventArgs.Empty);
-            }
-        }
-        
-        protected virtual void OnCancelRequested ()
-        {
-            IsCancelRequested = true;
-            
-            EventHandler handler = CancelRequested;
-            if (handler != null) {
-                handler (this, EventArgs.Empty);
-            }
-        }
-        
-        public virtual string Title {
-            get { return title; }
-            set { 
-                title = value; 
-                OnUpdated (); 
-            }
-        }
-        
-        public virtual string Status {
-            get { return status; }
-            set { 
-                status = value; 
-                OnUpdated (); 
-            }
-        }
-        
-        public virtual double Progress {
-            get { return progress; }
-            set { 
-                progress = Math.Max (0.0, Math.Min (1.0, value)); 
-                OnUpdated (); 
-            }
-        }
-        
-        public virtual string [] IconNames {
-            get { return icon_names; }
-            set {
-                if (value != null) {
-                    icon_names = value; 
-                    OnUpdated (); 
-                }
-            }
-        }
-
-        public virtual bool IsBackground {
-            get { return is_background; }
-            set { is_background = value; }
-        }
-        
-        public virtual string CancelMessage {
-            get { return cancel_message; }
-            set { cancel_message = value; }
-        }
-        
-        public virtual bool CanCancel {
-            get { return can_cancel; }
-            set {
-                can_cancel = value;
-                OnUpdated ();
-            }
-        }
-        
-        public virtual bool IsCancelRequested {
-            get { return is_cancel_requested; }
-            set { is_cancel_requested = value; }
-        }
-        
-        public virtual bool IsFinished {
-            get { return is_finished; }
-        }
-        
-        public virtual bool DelayShow {
-            get { return delay_show; }
-            set { delay_show = value; }
+            OnFinished ();
         }
     }
 }
diff --git a/src/Core/Banshee.Services/Banshee.ServiceStack/UserJobManager.cs b/src/Core/Banshee.Services/Banshee.ServiceStack/UserJobManager.cs
deleted file mode 100644
index ffafdb2..0000000
--- a/src/Core/Banshee.Services/Banshee.ServiceStack/UserJobManager.cs
+++ /dev/null
@@ -1,99 +0,0 @@
-// 
-// UserJobManager.cs
-//
-// Author:
-//   Aaron Bockover <abockover novell com>
-//
-// Copyright (C) 2007-2008 Novell, Inc.
-//
-// Permission is hereby granted, free of charge, to any person obtaining
-// a copy of this software and associated documentation files (the
-// "Software"), to deal in the Software without restriction, including
-// without limitation the rights to use, copy, modify, merge, publish,
-// distribute, sublicense, and/or sell copies of the Software, and to
-// permit persons to whom the Software is furnished to do so, subject to
-// the following conditions:
-//
-// The above copyright notice and this permission notice shall be
-// included in all copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
-// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
-// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
-// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-//
-
-using System;
-using System.Collections.Generic;
-
-namespace Banshee.ServiceStack
-{
-    public class UserJobManager : IRequiredService, IEnumerable<IUserJob>
-    {
-        private List<IUserJob> user_jobs = new List<IUserJob> ();
-        
-        public event UserJobEventHandler JobAdded;
-        public event UserJobEventHandler JobRemoved;
-        
-        public UserJobManager ()
-        {
-        }
-        
-        public void Register (IUserJob job)
-        {
-            lock (this) {
-                user_jobs.Add (job);
-                job.Finished += OnJobFinished;
-            }
-            
-            OnJobAdded (job);
-        }
-        
-        private void OnJobFinished (object o, EventArgs args)
-        {
-            lock (this) {
-                IUserJob job = (IUserJob)o;
-                
-                if (user_jobs.Contains (job)) {
-                    user_jobs.Remove (job);
-                }
-                
-                job.Finished -= OnJobFinished;
-                OnJobRemoved (job);
-            }
-        }
-        
-        protected virtual void OnJobAdded (IUserJob job)
-        {
-            UserJobEventHandler handler = JobAdded;
-            if (handler != null) {
-                handler (this, new UserJobEventArgs (job));
-            }
-        }
-        
-        protected virtual void OnJobRemoved (IUserJob job)
-        {
-            UserJobEventHandler handler = JobRemoved;
-            if (handler != null) {
-                handler (this, new UserJobEventArgs (job));
-            }
-        }
-        
-        public IEnumerator<IUserJob> GetEnumerator ()
-        {
-            return user_jobs.GetEnumerator ();
-        }
-        
-        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator ()
-        {
-            return GetEnumerator ();
-        }
-    
-        string IService.ServiceName {
-            get { return "UserJobManager"; }
-        }
-    }
-}
diff --git a/src/Core/Banshee.Services/Banshee.Sources/PrimarySource.cs b/src/Core/Banshee.Services/Banshee.Sources/PrimarySource.cs
index ce43fce..9bf98d6 100644
--- a/src/Core/Banshee.Services/Banshee.Sources/PrimarySource.cs
+++ b/src/Core/Banshee.Services/Banshee.Sources/PrimarySource.cs
@@ -31,6 +31,7 @@ using System.Collections.Generic;
 using Mono.Unix;
 
 using Hyena;
+using Hyena.Jobs;
 using Hyena.Data;
 using Hyena.Query;
 using Hyena.Data.Sqlite;
@@ -671,10 +672,10 @@ namespace Banshee.Sources
             set { delay_add_job = value; }
         }
 
-        private bool delay_delete_jbo = true;
+        private bool delay_delete_job = true;
         protected bool DelayDeleteJob {
-            get { return delay_delete_jbo; }
-            set { delay_delete_jbo = value; }
+            get { return delay_delete_job; }
+            set { delay_delete_job = value; }
         }
 
         private BatchUserJob add_track_job;
@@ -685,6 +686,8 @@ namespace Banshee.Sources
                         add_track_job = new BatchUserJob (String.Format (Catalog.GetString (
                             "Adding {0} of {1} to {2}"), "{0}", "{1}", Name), 
                             Properties.GetStringList ("Icon.Name"));
+                        add_track_job.SetResources (Resource.Cpu, Resource.Database, Resource.Disk);
+                        add_track_job.PriorityHints = PriorityHints.SpeedSensitive | PriorityHints.DataLossIfStopped;
                         add_track_job.DelayShow = DelayAddJob;
                         add_track_job.CanCancel = true;
                         add_track_job.Register ();
@@ -702,6 +705,8 @@ namespace Banshee.Sources
                         delete_track_job = new BatchUserJob (String.Format (Catalog.GetString (
                             "Deleting {0} of {1} From {2}"), "{0}", "{1}", Name),
                             Properties.GetStringList ("Icon.Name"));
+                        delete_track_job.SetResources (Resource.Cpu, Resource.Database);
+                        delete_track_job.PriorityHints = PriorityHints.SpeedSensitive | PriorityHints.DataLossIfStopped;
                         delete_track_job.DelayShow = DelayDeleteJob;
                         delete_track_job.Register ();
                     }
diff --git a/src/Core/Banshee.Services/Makefile.am b/src/Core/Banshee.Services/Makefile.am
index b1bd49e..adfbef4 100644
--- a/src/Core/Banshee.Services/Makefile.am
+++ b/src/Core/Banshee.Services/Makefile.am
@@ -168,12 +168,12 @@ SOURCES =  \
 	Banshee.ServiceStack/IRequiredService.cs \
 	Banshee.ServiceStack/IService.cs \
 	Banshee.ServiceStack/IUserJob.cs \
+	Banshee.ServiceStack/JobScheduler.cs \
 	Banshee.ServiceStack/ServiceManager.cs \
 	Banshee.ServiceStack/ServiceStartedHandler.cs \
 	Banshee.ServiceStack/TestUserJob.cs \
 	Banshee.ServiceStack/UserJob.cs \
 	Banshee.ServiceStack/UserJobEventHandler.cs \
-	Banshee.ServiceStack/UserJobManager.cs \
 	Banshee.SmartPlaylist/Migrator.cs \
 	Banshee.SmartPlaylist/SmartPlaylistDefinition.cs \
 	Banshee.SmartPlaylist/SmartPlaylistSource.cs \
diff --git a/src/Core/Banshee.ThickClient/Banshee.Gui.Dialogs/ConfirmShutdownDialog.cs b/src/Core/Banshee.ThickClient/Banshee.Gui.Dialogs/ConfirmShutdownDialog.cs
index 52213bb..3aa7f43 100644
--- a/src/Core/Banshee.ThickClient/Banshee.Gui.Dialogs/ConfirmShutdownDialog.cs
+++ b/src/Core/Banshee.ThickClient/Banshee.Gui.Dialogs/ConfirmShutdownDialog.cs
@@ -30,15 +30,19 @@ using System;
 using Gtk;
 using Mono.Unix;
 
-using Banshee.Kernel;
+using Hyena.Jobs;
+
+using Banshee.ServiceStack;
 
 namespace Banshee.Gui.Dialogs
 {
     public class ConfirmShutdownDialog : ErrorListDialog
     {
+        private Scheduler scheduler;
+
         public ConfirmShutdownDialog() : base()
         {
-            ListView.Model = new ListStore(typeof(string), typeof(IJob));
+            ListView.Model = new ListStore(typeof(string), typeof(Job));
             ListView.AppendColumn("Error", new CellRendererText(), "text", 0);
             ListView.HeadersVisible = false;
 
@@ -53,14 +57,14 @@ namespace Banshee.Gui.Dialogs
             
             AddButton(Catalog.GetString("Quit anyway"), ResponseType.Ok, false);
             AddButton(Catalog.GetString("Continue running"), ResponseType.Cancel, true);
+
+            scheduler = ServiceManager.JobScheduler;
+            foreach (Job job in scheduler.Jobs) {
+                AddJob (job);
+            }
             
-            foreach(IJob job in Scheduler.ScheduledJobs) {
-                AddJob(job);
-            }     
-            
-            Scheduler.JobScheduled += AddJob;
-            Scheduler.JobUnscheduled += RemoveJob;
-            Scheduler.JobFinished += RemoveJob;
+            scheduler.JobAdded += AddJob;
+            scheduler.JobRemoved += RemoveJob;
         }
         
         public void AddString(string message)
@@ -68,20 +72,20 @@ namespace Banshee.Gui.Dialogs
             (ListView.Model as ListStore).AppendValues(message, null);
         }
 
-        private void AddJob(IJob job)
+        private void AddJob(Job job)
         {
-            if(job is IInstanceCriticalJob) {
+            if (job.Has (PriorityHints.DataLossIfStopped)) {
                 Banshee.Base.ThreadAssist.ProxyToMain(delegate {
                     TreeIter iter = (ListView.Model as ListStore).Prepend();
-                    (ListView.Model as ListStore).SetValue(iter, 0, (job as IInstanceCriticalJob).Name);
+                    (ListView.Model as ListStore).SetValue(iter, 0, job.Title);
                     (ListView.Model as ListStore).SetValue(iter, 1, job);
                 });
             }
         }
 
-        private void RemoveJob(IJob job)
+        private void RemoveJob(Job job)
         {
-            if(!Scheduler.IsInstanceCriticalJobScheduled) {
+            if(!scheduler.HasAnyDataLossJobs) {
                 Dialog.Respond(Gtk.ResponseType.Ok);
                 return;
             }
diff --git a/src/Core/Banshee.ThickClient/Banshee.Gui.Widgets/TaskStatusIcon.cs b/src/Core/Banshee.ThickClient/Banshee.Gui.Widgets/TaskStatusIcon.cs
index 4156f5a..416505e 100644
--- a/src/Core/Banshee.ThickClient/Banshee.Gui.Widgets/TaskStatusIcon.cs
+++ b/src/Core/Banshee.ThickClient/Banshee.Gui.Widgets/TaskStatusIcon.cs
@@ -35,6 +35,7 @@ using Mono.Unix;
 using Gtk;
 
 using Hyena;
+using Hyena.Jobs;
 using Hyena.Widgets;
 
 using Banshee.Base;
@@ -44,8 +45,7 @@ namespace Banshee.Gui.Widgets
 {
     public class TaskStatusIcon : AnimatedImage
     {
-        //private Dictionary<IUserJob, UserJobTile> job_tiles = new Dictionary<IUserJob, UserJobTile> ();
-        private List<IUserJob> jobs = new List<IUserJob> ();
+        private List<Job> jobs = new List<Job> ();
 
         public TaskStatusIcon ()
         {
@@ -61,7 +61,7 @@ namespace Banshee.Gui.Widgets
             }
 
             // Listen for jobs
-            UserJobManager job_manager = ServiceManager.Get<UserJobManager> ();
+            JobScheduler job_manager = ServiceManager.Get<JobScheduler> ();
             job_manager.JobAdded += OnJobAdded;
             job_manager.JobRemoved += OnJobRemoved;
 
@@ -73,7 +73,7 @@ namespace Banshee.Gui.Widgets
             lock (jobs) {
                 if (jobs.Count > 0) {
                     StringBuilder sb = new StringBuilder ();
-                    foreach (IUserJob job in jobs) {
+                    foreach (Job job in jobs) {
                         sb.AppendFormat ("\n<i>{0}</i>", job.Title);
                     }
 
@@ -131,7 +131,7 @@ namespace Banshee.Gui.Widgets
             return false;
         }
 
-        private void AddJob (IUserJob job)
+        private void AddJob (Job job)
         {                
             lock (jobs) {    
                 if (job == null || !job.IsBackground || job.IsFinished) {
@@ -144,12 +144,12 @@ namespace Banshee.Gui.Widgets
             ThreadAssist.ProxyToMain (Update);
         }
         
-        private void OnJobAdded (object o, UserJobEventArgs args)
+        private void OnJobAdded (Job job)
         {
-            AddJob (args.Job);
+            AddJob (job);
         }
         
-        private void RemoveJob (IUserJob job)
+        private void RemoveJob (Job job)
         {
             lock (jobs) {
                 if (jobs.Contains (job)) {
@@ -160,9 +160,9 @@ namespace Banshee.Gui.Widgets
             ThreadAssist.ProxyToMain (Update);
         }
         
-        private void OnJobRemoved (object o, UserJobEventArgs args)
+        private void OnJobRemoved (Job job)
         {
-            RemoveJob (args.Job);
+            RemoveJob (job);
         }
     }
 }
diff --git a/src/Core/Banshee.ThickClient/Banshee.Gui.Widgets/UserJobTile.cs b/src/Core/Banshee.ThickClient/Banshee.Gui.Widgets/UserJobTile.cs
index 24ef9d6..1a1eb42 100644
--- a/src/Core/Banshee.ThickClient/Banshee.Gui.Widgets/UserJobTile.cs
+++ b/src/Core/Banshee.ThickClient/Banshee.Gui.Widgets/UserJobTile.cs
@@ -30,15 +30,17 @@ using System;
 using Mono.Unix;
 using Gtk;
 
+using Hyena.Jobs;
+using Hyena.Gui;
+
 using Banshee.Base;
 using Banshee.ServiceStack;
-using Hyena.Gui;
 
 namespace Banshee.Gui.Widgets
 {
     public class UserJobTile : Table
     {
-        private IUserJob job;
+        private Job job;
         
         private string [] icon_names;
         private string title;
@@ -56,7 +58,7 @@ namespace Banshee.Gui.Widgets
         
         Banshee.Widgets.HigMessageDialog cancel_dialog;
         
-        public UserJobTile (IUserJob job) : base (3, 2, false)
+        public UserJobTile (Job job) : base (3, 2, false)
         {
             this.job = job;
             this.job.Updated += OnJobUpdated;
@@ -151,7 +153,7 @@ namespace Banshee.Gui.Widgets
                 
             if (cancel_dialog.Run () == (int)ResponseType.Yes) {
                 if (job.CanCancel) {
-                    job.Cancel ();
+                    ServiceManager.JobScheduler.Cancel (job);
                 }
             }
         
diff --git a/src/Core/Banshee.ThickClient/Banshee.Gui.Widgets/UserJobTileHost.cs b/src/Core/Banshee.ThickClient/Banshee.Gui.Widgets/UserJobTileHost.cs
index c118334..ffc1b05 100644
--- a/src/Core/Banshee.ThickClient/Banshee.Gui.Widgets/UserJobTileHost.cs
+++ b/src/Core/Banshee.ThickClient/Banshee.Gui.Widgets/UserJobTileHost.cs
@@ -31,6 +31,7 @@ using System.Collections.Generic;
 
 using Gtk;
 
+using Hyena.Jobs;
 using Hyena.Widgets;
 using Hyena.Gui.Theatrics;
 
@@ -42,8 +43,8 @@ namespace Banshee.Gui.Widgets
     public class UserJobTileHost : Alignment
     {
         private AnimatedVBox box;
-        private Dictionary<IUserJob, UserJobTile> job_tiles = new Dictionary<IUserJob, UserJobTile> ();
-        private Dictionary<IUserJob, DateTime> job_start_times = new Dictionary<IUserJob, DateTime> ();
+        private Dictionary<Job, UserJobTile> job_tiles = new Dictionary<Job, UserJobTile> ();
+        private Dictionary<Job, DateTime> job_start_times = new Dictionary<Job, DateTime> ();
         
         public UserJobTileHost () : base (0.0f, 0.0f, 1.0f, 1.0f)
         {
@@ -57,8 +58,8 @@ namespace Banshee.Gui.Widgets
             Add (box);
             ShowAll ();
 
-            if (ServiceManager.Contains<UserJobManager> ()) {
-                UserJobManager job_manager = ServiceManager.Get<UserJobManager> ();
+            if (ServiceManager.Contains<JobScheduler> ()) {
+                JobScheduler job_manager = ServiceManager.Get<JobScheduler> ();
                 job_manager.JobAdded += OnJobAdded;
                 job_manager.JobRemoved += OnJobRemoved;
             }
@@ -72,7 +73,7 @@ namespace Banshee.Gui.Widgets
             }
         }
 
-        private void AddJob (IUserJob job)
+        private void AddJob (Job job)
         {                
             lock (this) {    
                 if (job == null || job.IsFinished) {
@@ -90,26 +91,26 @@ namespace Banshee.Gui.Widgets
             }
         }
         
-        private void OnJobAdded (object o, UserJobEventArgs args)
+        private void OnJobAdded (Job job)
         {
-            if (args.Job.IsBackground) {
+            if (job.IsBackground) {
                 return;
             }
 
             ThreadAssist.ProxyToMain (delegate {
-                if (args.Job.DelayShow) {
+                if (job.DelayShow) {
                     // Give the Job 1 second to become more than 33% complete
                     Banshee.ServiceStack.Application.RunTimeout (1000, delegate {
-                        AddJob (args.Job);
+                        AddJob (job);
                         return false;
                     });
                 } else {
-                    AddJob (args.Job);
+                    AddJob (job);
                 }
             });
         }
         
-        private void RemoveJob (IUserJob job)
+        private void RemoveJob (Job job)
         {
             lock (this) {
                 if (job_tiles.ContainsKey (job)) {
@@ -122,23 +123,23 @@ namespace Banshee.Gui.Widgets
             }
         }
         
-        private void OnJobRemoved (object o, UserJobEventArgs args)
+        private void OnJobRemoved (Job job)
         {
             ThreadAssist.ProxyToMain (delegate {
                 lock (this) {
-                    if (job_start_times.ContainsKey (args.Job)) {
-                        double ms_since_added = (DateTime.Now - job_start_times[args.Job]).TotalMilliseconds;
+                    if (job_start_times.ContainsKey (job)) {
+                        double ms_since_added = (DateTime.Now - job_start_times[job]).TotalMilliseconds;
                         if (ms_since_added < 1000) {
                             // To avoid user jobs flasing up and out, don't let any job be visible for less than 1 second
                             Banshee.ServiceStack.Application.RunTimeout ((uint) (1000 - ms_since_added), delegate {
-                                RemoveJob (args.Job);
+                                RemoveJob (job);
                                 return false;
                             });
                             return;
                         }
                     }
                     
-                    RemoveJob (args.Job);
+                    RemoveJob (job);
                 }
             });
         }
diff --git a/src/Dap/Banshee.Dap.Ipod/Banshee.Dap.Ipod/DatabaseRebuilder.cs b/src/Dap/Banshee.Dap.Ipod/Banshee.Dap.Ipod/DatabaseRebuilder.cs
index 1534345..53ad203 100644
--- a/src/Dap/Banshee.Dap.Ipod/Banshee.Dap.Ipod/DatabaseRebuilder.cs
+++ b/src/Dap/Banshee.Dap.Ipod/Banshee.Dap.Ipod/DatabaseRebuilder.cs
@@ -34,6 +34,7 @@ using Mono.Unix;
 using IPod;
 
 using Hyena;
+using Hyena.Jobs;
 
 using Banshee.Base;
 using Banshee.ServiceStack;
@@ -89,6 +90,8 @@ namespace Banshee.Dap.Ipod
             this.source = source;
             
             user_job = new UserJob (Catalog.GetString ("Rebuilding Database"));
+            user_job.PriorityHints = PriorityHints.SpeedSensitive | PriorityHints.DataLossIfStopped;
+            user_job.SetResources (Resource.Disk, Resource.Cpu);
             user_job.Title = Catalog.GetString ("Rebuilding Database");
             user_job.Status = Catalog.GetString ("Scanning iPod...");
             user_job.IconNames = source._GetIconNames ();
diff --git a/src/Extensions/Banshee.AudioCd/Banshee.AudioCd/AudioCdRipper.cs b/src/Extensions/Banshee.AudioCd/Banshee.AudioCd/AudioCdRipper.cs
index 3a2af96..370a4ab 100644
--- a/src/Extensions/Banshee.AudioCd/Banshee.AudioCd/AudioCdRipper.cs
+++ b/src/Extensions/Banshee.AudioCd/Banshee.AudioCd/AudioCdRipper.cs
@@ -32,6 +32,8 @@ using System.Collections.Generic;
 using Mono.Unix;
 using Mono.Addins;
 
+using Hyena.Jobs;
+
 using Banshee.Base;
 using Banshee.ServiceStack;
 using Banshee.Collection;
@@ -119,6 +121,8 @@ namespace Banshee.AudioCd
             user_job.CancelMessage = String.Format (Catalog.GetString (
                 "<i>{0}</i> is still being imported into the music library. Would you like to stop it?"
                 ), GLib.Markup.EscapeText (source.DiscModel.Title));
+            user_job.SetResources (Resource.Cpu);
+            user_job.PriorityHints = PriorityHints.SpeedSensitive | PriorityHints.DataLossIfStopped;
             user_job.CanCancel = true;
             user_job.CancelRequested += OnCancelRequested;
             user_job.Finished += OnFinished;
diff --git a/src/Extensions/Banshee.Bpm/Banshee.Bpm/BpmDetectJob.cs b/src/Extensions/Banshee.Bpm/Banshee.Bpm/BpmDetectJob.cs
index b65ed85..e673d3a 100644
--- a/src/Extensions/Banshee.Bpm/Banshee.Bpm/BpmDetectJob.cs
+++ b/src/Extensions/Banshee.Bpm/Banshee.Bpm/BpmDetectJob.cs
@@ -35,6 +35,7 @@ using Mono.Unix;
 using Mono.Addins;
 
 using Hyena;
+using Hyena.Jobs;
 using Hyena.Data.Sqlite;
 
 using Banshee.Base;
@@ -53,6 +54,9 @@ namespace Banshee.Bpm
         private int current_track_id;
         private IBpmDetector detector;
         private PrimarySource music_library;
+        private ManualResetEvent result_ready_event = new ManualResetEvent (false);
+        private SafeUri result_uri;
+        private int result_bpm;
         
         private static HyenaSqliteCommand update_query = new HyenaSqliteCommand (
             "UPDATE CoreTracks SET BPM = ?, DateUpdatedStamp = ? WHERE TrackID = ?");
@@ -61,6 +65,8 @@ namespace Banshee.Bpm
         {
             IconNames = new string [] {"audio-x-generic"};
             IsBackground = true;
+            SetResources (Resource.Cpu, Resource.Disk);
+            PriorityHints = PriorityHints.LongRunning;
 
             music_library = ServiceManager.SourceManager.MusicLibrary;
 
@@ -85,33 +91,52 @@ namespace Banshee.Bpm
             detector.FileFinished += OnFileFinished;
         }
 
-        protected override void Cleanup ()
+        protected override void OnCancelled ()
         {
-            Finish ();
+            Cleanup ();
+            result_ready_event.Set ();
+        }
 
+        protected override void Cleanup ()
+        {
             if (detector != null) {
+                detector.FileFinished -= OnFileFinished;
                 detector.Dispose ();
+                detector = null;
             }
+
+            base.Cleanup ();
         }
 
         protected override void IterateCore (HyenaDataReader reader)
         {
             SafeUri uri = new SafeUri (reader.Get<string> (0));
             current_track_id = reader.Get<int> (1);
+
+            // Wait for the result to be ready
+            result_ready_event.Reset ();
             detector.ProcessFile (uri);
-        }
+            result_ready_event.WaitOne ();
 
-        private void OnFileFinished (SafeUri uri, int bpm)
-        {
-            if (bpm > 0) {
-                Log.DebugFormat ("Saving BPM of {0} for {1}", bpm, uri);
-                ServiceManager.DbConnection.Execute (update_query, bpm, DateTime.Now, current_track_id);
+            if (IsCancelRequested) {
+                return;
+            }
+
+            if (result_bpm > 0) {
+                Log.DebugFormat ("Saving BPM of {0} for {1}", result_bpm, result_uri);
+                ServiceManager.DbConnection.Execute (update_query, result_bpm, DateTime.Now, current_track_id);
             } else {
                 ServiceManager.DbConnection.Execute (update_query, -1, DateTime.Now, current_track_id);
-                Log.DebugFormat ("Unable to detect BPM for {0}", uri);
+                Log.DebugFormat ("Unable to detect BPM for {0}", result_uri);
             }
+        }
 
-            Iterate ();
+        private void OnFileFinished (SafeUri uri, int bpm)
+        {
+            // This is run on the main thread b/c of GStreamer, so do as little as possible here
+            result_uri = uri;
+            result_bpm = bpm;
+            result_ready_event.Set ();
         }
 
         internal static IBpmDetector GetDetector ()
diff --git a/src/Extensions/Banshee.Bpm/Banshee.Bpm/BpmService.cs b/src/Extensions/Banshee.Bpm/Banshee.Bpm/BpmService.cs
index 1d719cf..1e0f8f3 100644
--- a/src/Extensions/Banshee.Bpm/Banshee.Bpm/BpmService.cs
+++ b/src/Extensions/Banshee.Bpm/Banshee.Bpm/BpmService.cs
@@ -121,7 +121,6 @@ namespace Banshee.Bpm
             }
 
             job.Finished += delegate { job = null; };
-            job.RunAsync ();
         }
         
         private void OnTracksAdded (Source sender, TrackEventArgs args)
@@ -167,7 +166,7 @@ namespace Banshee.Bpm
                     Detect ();
                 } else {
                     if (job != null) {
-                        job.Cancel ();
+                        ServiceManager.JobScheduler.Cancel (job);
                     }
                 }
             }
diff --git a/src/Extensions/Banshee.CoverArt/Banshee.CoverArt/CoverArtJob.cs b/src/Extensions/Banshee.CoverArt/Banshee.CoverArt/CoverArtJob.cs
index b385b95..47171b0 100644
--- a/src/Extensions/Banshee.CoverArt/Banshee.CoverArt/CoverArtJob.cs
+++ b/src/Extensions/Banshee.CoverArt/Banshee.CoverArt/CoverArtJob.cs
@@ -36,6 +36,7 @@ using Mono.Unix;
 using Gtk;
 
 using Hyena;
+using Hyena.Jobs;
 using Hyena.Data.Sqlite;
 
 using Banshee.Base;
@@ -49,37 +50,10 @@ using Banshee.Library;
 
 namespace Banshee.CoverArt
 {
-    public class CoverArtJob : UserJob, IJob
+    public class CoverArtJob : DbIteratorJob
     {
-        private const int BatchSize = 10;
-        
         private DateTime last_scan = DateTime.MinValue;
         private TimeSpan retry_every = TimeSpan.FromDays (7);
-
-        private static HyenaSqliteCommand count_query = new HyenaSqliteCommand (@"
-            SELECT count(DISTINCT CoreTracks.AlbumID)
-            FROM CoreTracks, CoreArtists, CoreAlbums
-            WHERE
-                CoreTracks.PrimarySourceID = ? AND
-                CoreTracks.DateUpdatedStamp > ? AND
-                CoreTracks.AlbumID = CoreAlbums.AlbumID AND 
-                CoreAlbums.ArtistID = CoreArtists.ArtistID AND
-                CoreTracks.AlbumID NOT IN (
-                    SELECT AlbumID FROM CoverArtDownloads WHERE
-                        LastAttempt > ? OR Downloaded = 1)");
-
-        private static HyenaSqliteCommand select_query = new HyenaSqliteCommand (@"
-            SELECT DISTINCT CoreAlbums.AlbumID, CoreAlbums.Title, CoreArtists.Name, CoreTracks.Uri
-            FROM CoreTracks, CoreArtists, CoreAlbums
-            WHERE
-                CoreTracks.PrimarySourceID = ? AND
-                CoreTracks.DateUpdatedStamp > ? AND
-                CoreTracks.AlbumID = CoreAlbums.AlbumID AND 
-                CoreAlbums.ArtistID = CoreArtists.ArtistID AND
-                CoreTracks.AlbumID NOT IN (
-                    SELECT AlbumID FROM CoverArtDownloads WHERE
-                        LastAttempt > ? OR Downloaded = 1)
-            GROUP BY CoreTracks.AlbumID LIMIT ?");
         
         public CoverArtJob (DateTime lastScan) : base (Catalog.GetString ("Downloading Cover Art"))
         {
@@ -91,75 +65,67 @@ namespace Banshee.CoverArt
                 last_scan = DateTime.Now - TimeSpan.FromDays (300);
             }
 
+            CountCommand = new HyenaSqliteCommand (@"
+                SELECT count(DISTINCT CoreTracks.AlbumID)
+                    FROM CoreTracks, CoreArtists, CoreAlbums
+                    WHERE
+                        CoreTracks.PrimarySourceID = ? AND
+                        CoreTracks.DateUpdatedStamp > ? AND
+                        CoreTracks.AlbumID = CoreAlbums.AlbumID AND 
+                        CoreAlbums.ArtistID = CoreArtists.ArtistID AND
+                        CoreTracks.AlbumID NOT IN (
+                            SELECT AlbumID FROM CoverArtDownloads WHERE
+                                LastAttempt > ? OR Downloaded = 1)",
+                ServiceManager.SourceManager.MusicLibrary.DbId, last_scan, last_scan - retry_every
+            );
+
+            SelectCommand = new HyenaSqliteCommand (@"
+                SELECT DISTINCT CoreAlbums.AlbumID, CoreAlbums.Title, CoreArtists.Name, CoreTracks.Uri 
+                    FROM CoreTracks, CoreArtists, CoreAlbums
+                    WHERE
+                        CoreTracks.PrimarySourceID = ? AND
+                        CoreTracks.DateUpdatedStamp > ? AND
+                        CoreTracks.AlbumID = CoreAlbums.AlbumID AND 
+                        CoreAlbums.ArtistID = CoreArtists.ArtistID AND
+                        CoreTracks.AlbumID NOT IN (
+                            SELECT AlbumID FROM CoverArtDownloads WHERE
+                                LastAttempt > ? OR Downloaded = 1)
+                    GROUP BY CoreTracks.AlbumID LIMIT ?",
+                ServiceManager.SourceManager.MusicLibrary.DbId, last_scan, last_scan - retry_every, 1
+            );
+
+            SetResources (Resource.Database);
+            PriorityHints = PriorityHints.LongRunning;
+
             IsBackground = true;
             CanCancel = true;
             DelayShow = true;
-
-            CancelRequested += delegate {
-                Finish ();
-            };
         }
         
         public void Start ()
         {
             Register ();
-            Scheduler.Schedule (this, JobPriority.Lowest);
         }
 
-        private HyenaDataReader RunQuery ()
+        protected override void IterateCore (HyenaDataReader reader)
         {
-            return new HyenaDataReader (ServiceManager.DbConnection.Query (select_query,
-                ServiceManager.SourceManager.MusicLibrary.DbId, last_scan, last_scan - retry_every, BatchSize
-            ));
-        }
-        
-        public void Run ()
-        {
-            Status = Catalog.GetString ("Preparing...");
-            IconNames = new string [] {Stock.Network};
-            
-            int current = 0;
-            int total = 0;
+            DatabaseTrackInfo track = new DatabaseTrackInfo ();
 
-            try {
-                DatabaseTrackInfo track = new DatabaseTrackInfo ();
-                while (true) {
-                    total = current + ServiceManager.DbConnection.Query<int> (count_query, ServiceManager.SourceManager.MusicLibrary.DbId, last_scan, last_scan - retry_every);
-                    if (total == 0 || total <= current) {
-                        break;
-                    }
-
-                    using (HyenaDataReader reader = RunQuery ()) {
-                        while (reader.Read ()) {
-                            if (IsCancelRequested) {
-                                Finish ();
-                                return;
-                            }
-                            
-                            track.AlbumTitle = reader.Get<string> (1);
-                            track.ArtistName = reader.Get<string> (2);
-                            track.PrimarySource = ServiceManager.SourceManager.MusicLibrary;
-                            track.Uri = new SafeUri (reader.Get<string> (3));
-                            track.AlbumId = reader.Get<int> (0);
-                            //Console.WriteLine ("have album {0}/{1} for track uri {2}", track.AlbumId, track.AlbumTitle, track.Uri);
-
-                            Progress = (double) current / (double) total;
-                            Status = String.Format (Catalog.GetString ("{0} - {1}"), track.ArtistName, track.AlbumTitle);
-
-                            FetchForTrack (track);
-                            current++;
-                        }
-                    }
-                }
-            } catch (Exception e) {
-                Log.Exception (e);
-            }
- 
-            Finish ();
+            track.AlbumTitle = reader.Get<string> (1);
+            track.ArtistName = reader.Get<string> (2);
+            track.PrimarySource = ServiceManager.SourceManager.MusicLibrary;
+            track.Uri = new SafeUri (reader.Get<string> (3));
+            track.AlbumId = reader.Get<int> (0);
+            //Console.WriteLine ("have album {0}/{1} for track uri {2}", track.AlbumId, track.AlbumTitle, track.Uri);
+
+            Status = String.Format (Catalog.GetString ("{0} - {1}"), track.ArtistName, track.AlbumTitle);
+
+            FetchForTrack (track);
         }
 
         private void FetchForTrack (DatabaseTrackInfo track)
         {
+            bool save = true;
             try {
                 if (String.IsNullOrEmpty (track.AlbumTitle) || track.AlbumTitle == Catalog.GetString ("Unknown Album") ||
                     String.IsNullOrEmpty (track.ArtistName) || track.ArtistName == Catalog.GetString ("Unknown Artist")) {
@@ -168,14 +134,24 @@ namespace Banshee.CoverArt
                     IMetadataLookupJob job = MetadataService.Instance.CreateJob (track);
                     job.Run ();
                 }
+            } catch (System.Threading.ThreadAbortException) {
+                save = false;
+                throw;
             } catch (Exception e) {
                 Log.Exception (e);
             } finally {
-                bool have_cover_art = CoverArtSpec.CoverExists (track.ArtistName, track.AlbumTitle);
-                ServiceManager.DbConnection.Execute (
-                    "INSERT OR REPLACE INTO CoverArtDownloads (AlbumID, Downloaded, LastAttempt) VALUES (?, ?, ?)",
-                    track.AlbumId, have_cover_art, DateTime.Now);
+                if (save) {
+                    bool have_cover_art = CoverArtSpec.CoverExists (track.ArtistName, track.AlbumTitle);
+                    ServiceManager.DbConnection.Execute (
+                        "INSERT OR REPLACE INTO CoverArtDownloads (AlbumID, Downloaded, LastAttempt) VALUES (?, ?, ?)",
+                        track.AlbumId, have_cover_art, DateTime.Now);
+                }
             }
         }
+
+        protected override void OnCancelled ()
+        {
+            AbortThread ();
+        }
     }
 }
diff --git a/src/Extensions/Banshee.CoverArt/Banshee.CoverArt/CoverArtService.cs b/src/Extensions/Banshee.CoverArt/Banshee.CoverArt/CoverArtService.cs
index ac063aa..1e57654 100644
--- a/src/Extensions/Banshee.CoverArt/Banshee.CoverArt/CoverArtService.cs
+++ b/src/Extensions/Banshee.CoverArt/Banshee.CoverArt/CoverArtService.cs
@@ -172,8 +172,9 @@ namespace Banshee.CoverArt
                 }
                 job = new CoverArtJob (last_scan);
                 job.Finished += delegate {
-                    DatabaseConfigurationClient.Client.Set<DateTime> ("last_cover_art_scan",
-                                                                      DateTime.Now);
+                    if (!job.IsCancelRequested) {
+                        DatabaseConfigurationClient.Client.Set<DateTime> ("last_cover_art_scan", DateTime.Now);
+                    }
                     job = null;
                 };
                 job.Start ();
diff --git a/src/Libraries/Hyena/Hyena.Jobs/Job.cs b/src/Libraries/Hyena/Hyena.Jobs/Job.cs
new file mode 100644
index 0000000..cc477fc
--- /dev/null
+++ b/src/Libraries/Hyena/Hyena.Jobs/Job.cs
@@ -0,0 +1,312 @@
+//
+// Job.cs
+//
+// Author:
+//   Gabriel Burt <gburt novell com>
+//
+// Copyright (C) 2009 Novell, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+using System.Linq;
+using System.Threading;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+
+namespace Hyena.Jobs
+{
+    public enum JobState {
+        None,
+        Scheduled,
+        Running,
+        Paused,
+        Cancelled,
+        Completed
+    };
+
+    public class Job
+    {
+        public event EventHandler Updated;
+        public event EventHandler Finished;
+        public event EventHandler CancelRequested;
+
+        private int update_freeze_ref;
+        private JobState state = JobState.None;
+
+        private ManualResetEvent pause_event;
+        private DateTime created_at = DateTime.Now;
+        private TimeSpan run_time = TimeSpan.Zero;
+        private Object sync = new Object ();
+
+        public bool IsCancelRequested { get; private set; }
+
+#region Internal Properties
+
+        internal bool IsScheduled {
+            get { return state == JobState.Scheduled; }
+        }
+
+        internal bool IsRunning {
+            get { return state == JobState.Running; }
+        }
+
+        internal bool IsPaused {
+            get { return state == JobState.Paused; }
+        }
+
+        public bool IsFinished {
+            get {
+                lock (sync) {
+                    return state == JobState.Cancelled || state == JobState.Completed;
+                }
+            }
+        }
+
+        internal DateTime CreatedAt {
+            get { return created_at; }
+        }
+
+        internal TimeSpan RunTime {
+            get { return run_time; }
+        }
+
+#endregion
+
+#region Scheduler Methods
+
+        internal void Start ()
+        {
+            Log.Debug ("Starting", Title);
+            lock (sync) {
+                if (state != JobState.Scheduled && state != JobState.Paused) {
+                    Log.DebugFormat ("Job {0} in {1} state is not runnable", Title, state);
+                    return;
+                }
+
+                State = JobState.Running;
+
+                if (pause_event != null) {
+                    pause_event.Set ();
+                }
+
+                RunJob ();
+            }
+        }
+
+        internal void Cancel ()
+        {
+            lock (sync) {
+                if (!IsFinished) {
+                    IsCancelRequested = true;
+                    State = JobState.Cancelled;
+                    EventHandler handler = CancelRequested;
+                    if (handler != null) {
+                        handler (this, EventArgs.Empty);
+                    }
+                }
+            }
+            Log.Debug ("Canceled", Title);
+        }
+
+        internal void Preempt ()
+        {
+            Log.Debug ("Preemptd", Title);
+            Pause (false);
+        }
+
+        internal bool Pause ()
+        {
+            Log.Debug ("Pausing ", Title);
+            return Pause (true);
+        }
+
+        private bool Pause (bool unschedule)
+        {
+            lock (sync) {
+                if (IsFinished) {
+                    Log.DebugFormat ("Job {0} in {1} state is not pausable", Title, state);
+                    return false;
+                }
+
+                State = unschedule ? JobState.Paused : JobState.Scheduled;
+                if (pause_event != null) {
+                    pause_event.Reset ();
+                }
+            }
+
+            return true;
+        }
+
+#endregion
+
+        private string title;
+        private string status;
+        private string [] icon_names;
+        private double progress;
+
+#region Public Properties
+
+        public string Title {
+            get { return title; }
+            set {
+                title = value;
+                OnUpdated ();
+            }
+        }
+
+        public string Status {
+            get { return status; }
+            set {
+                status = value;
+                OnUpdated ();
+            }
+        }
+
+        public double Progress {
+            get { return progress; }
+            set {
+                progress = Math.Max (0.0, Math.Min (1.0, value));
+                OnUpdated ();
+            }
+        }
+
+        public string [] IconNames {
+            get { return icon_names; }
+            set {
+                if (value != null) {
+                    icon_names = value; 
+                    OnUpdated (); 
+                }
+            }
+        }
+
+        public bool IsBackground { get; set; }
+        public bool CanCancel { get; set; }
+        public string CancelMessage { get; set; }
+        public bool DelayShow { get; set; }
+
+        public PriorityHints PriorityHints { get; set; }
+        public IEnumerable<Resource> Resources { get; protected set; }
+
+        public JobState State {
+            get { return state; }
+            internal set {
+                state = value;
+                OnUpdated ();
+            }
+        }
+
+        public void SetResources (params Resource [] resources)
+        {
+            Resources = resources;
+        }
+
+#endregion
+
+#region Constructor
+
+        public Job () : this (null, PriorityHints.None)
+        {
+        }
+
+        public Job (string title, PriorityHints hints, params Resource [] resources)
+        {
+            Title = title;
+            PriorityHints = hints;
+            Resources = resources;
+        }
+
+#endregion
+
+#region Abstract Methods
+
+        protected virtual void RunJob ()
+        {
+        }
+
+#endregion
+
+#region Protected Methods
+
+        public void Update (string title, string status, double progress)
+        {
+            Title = title;
+            Status = status;
+            Progress = progress;
+        }
+
+        protected void FreezeUpdate ()
+        {
+            System.Threading.Interlocked.Increment (ref update_freeze_ref);
+        }
+
+        protected void ThawUpdate (bool raiseUpdate)
+        {
+            System.Threading.Interlocked.Decrement (ref update_freeze_ref);
+            if (raiseUpdate) {
+                OnUpdated ();
+            }
+        }
+
+        protected void OnUpdated ()
+        {
+            if (update_freeze_ref != 0) {
+                return;
+            }
+
+            EventHandler handler = Updated;
+            if (handler != null) {
+                handler (this, EventArgs.Empty);
+            }
+        }
+
+        public void YieldToScheduler ()
+        {
+            if (IsPaused || IsScheduled) {
+                if (pause_event == null) {
+                    pause_event = new ManualResetEvent (false);
+                }
+
+                pause_event.WaitOne ();
+            }
+        }
+
+        protected void OnFinished ()
+        {
+            Log.Debug ("Finished", Title);
+            pause_event = null;
+
+            if (state != JobState.Cancelled) {
+                State = JobState.Completed;
+            }
+
+            EventHandler handler = Finished;
+            if (handler != null) {
+                handler (this, EventArgs.Empty);
+            }
+        }
+
+#endregion
+
+        internal bool HasScheduler { get; set; }
+    }
+}
diff --git a/src/Libraries/Hyena/Hyena.Jobs/JobExtensions.cs b/src/Libraries/Hyena/Hyena.Jobs/JobExtensions.cs
new file mode 100644
index 0000000..0c15568
--- /dev/null
+++ b/src/Libraries/Hyena/Hyena.Jobs/JobExtensions.cs
@@ -0,0 +1,69 @@
+//
+// JobExtensions.cs
+//
+// Author:
+//   Gabriel Burt <gburt novell com>
+//
+// Copyright (C) 2009 Novell, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+using System.Linq;
+using System.Collections.Generic;
+
+namespace Hyena.Jobs
+{
+    public static class JobExtensions
+    {
+        internal static IEnumerable<T> Without<T> (this IEnumerable<T> source, PriorityHints hints) where T : Job
+        {
+            return source.Where (j => !j.Has (hints));
+        }
+
+        internal static IEnumerable<T> With<T> (this IEnumerable<T> source, PriorityHints hints) where T : Job
+        {
+            return source.Where (j => j.Has (hints));
+        }
+
+        internal static IEnumerable<T> SharingResourceWith<T> (this IEnumerable<T> source, Job job) where T : Job
+        {
+            return source.Where (j => j.Resources.Intersect (job.Resources).Any ());
+        }
+
+        public static void ForEach<T> (this IEnumerable<T> source, Action<T> func)
+        {
+            foreach (T item in source)
+                func (item);
+        }
+
+        public static bool Has<T> (this T job, PriorityHints hints) where T : Job
+        {
+            return (job.PriorityHints & hints) == hints;
+        }
+
+        // Useful..
+        /*public static bool Include (this Enum source, Enum flags)
+        {
+            return ((int)source & (int)flags) == (int)flags;
+        }*/
+    }
+}
diff --git a/src/Libraries/Hyena/Hyena.Jobs/PriorityHints.cs b/src/Libraries/Hyena/Hyena.Jobs/PriorityHints.cs
new file mode 100644
index 0000000..125b3b8
--- /dev/null
+++ b/src/Libraries/Hyena/Hyena.Jobs/PriorityHints.cs
@@ -0,0 +1,41 @@
+//
+// PriorityHints.cs
+//
+// Author:
+//   Gabriel Burt <gburt novell com>
+//
+// Copyright (C) 2009 Novell, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+
+namespace Hyena.Jobs
+{
+    [Flags]
+    public enum PriorityHints
+    {
+        None = 0,
+        DataLossIfStopped = 1,
+        SpeedSensitive = 2,
+        LongRunning = 4
+    }
+}
diff --git a/src/Libraries/Hyena/Hyena.Jobs/Resource.cs b/src/Libraries/Hyena/Hyena.Jobs/Resource.cs
new file mode 100644
index 0000000..bf99a97
--- /dev/null
+++ b/src/Libraries/Hyena/Hyena.Jobs/Resource.cs
@@ -0,0 +1,43 @@
+//
+// Resource.cs
+//
+// Author:
+//   Gabriel Burt <gburt novell com>
+//
+// Copyright (C) 2009 Novell, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+
+namespace Hyena.Jobs
+{
+    public class Resource
+    {
+        // Convenience Resources for programs to use
+        public static readonly Resource Cpu = new Resource { Id = "cpu", Name = "CPU" };
+        public static readonly Resource Disk = new Resource { Id = "disk", Name = "Disk" };
+        public static readonly Resource Database = new Resource { Id = "db", Name = "Database" };
+
+        public string Id { get; set; }
+        public string Name { get; set; }
+    }
+}
diff --git a/src/Libraries/Hyena/Hyena.Jobs/Scheduler.cs b/src/Libraries/Hyena/Hyena.Jobs/Scheduler.cs
new file mode 100644
index 0000000..d18f713
--- /dev/null
+++ b/src/Libraries/Hyena/Hyena.Jobs/Scheduler.cs
@@ -0,0 +1,230 @@
+//
+// Scheduler.cs
+//
+// Author:
+//   Gabriel Burt <gburt novell com>
+//
+// Copyright (C) 2009 Novell, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+using System.Linq;
+using System.Threading;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+
+using Hyena;
+
+namespace Hyena.Jobs
+{
+    public class Scheduler
+    {
+        private List<Job> jobs;
+
+        public event Action<Job> JobAdded;
+        public event Action<Job> JobRemoved;
+
+        public IEnumerable<Job> Jobs { get; private set; }
+
+        public int JobCount {
+            get { lock (jobs) { return jobs.Count; } }
+        }
+
+        public bool HasAnyDataLossJobs {
+            get {
+                lock (jobs) {
+                    return jobs.With (PriorityHints.DataLossIfStopped).Any ();
+                }
+            }
+        }
+
+        public Scheduler ()
+        {
+            jobs = new List<Job> ();
+            Jobs = new ReadOnlyCollection<Job> (jobs);
+        }
+
+        public void Add (Job job)
+        {
+            lock (jobs) {
+                lock (job) {
+                    if (jobs.Contains (job) || job.HasScheduler) {
+                        throw new ArgumentException ("Job not schedulable", "job");
+                    }
+
+                    job.HasScheduler = true;
+                }
+
+                jobs.Add (job);
+                job.State = JobState.Scheduled;
+                job.Finished += OnJobFinished;
+
+                if (CanStart (job)) {
+                    StartJob (job);
+                }
+            }
+
+            Action<Job> handler = JobAdded;
+            if (handler != null) {
+                handler (job);
+            }
+        }
+
+        public void Cancel (Job job)
+        {
+            lock (jobs) {
+                if (jobs.Contains (job)) {
+                    // Cancel will call OnJobFinished which will call Schedule
+                    job.Cancel ();
+                }
+            }
+        }
+
+        public void Pause (Job job)
+        {
+            lock (jobs) {
+                if (jobs.Contains (job)) {
+                    if (job.Pause ()) {
+                        // See if any scheduled jobs can now be started
+                        Schedule ();
+                    }
+                }
+            }
+        }
+
+        public void Resume (Job job)
+        {
+            lock (jobs) {
+                if (jobs.Contains (job) && CanStartJob (job, true)) {
+                    StartJob (job);
+                }
+            }
+        }
+
+        public void CancelAll (bool evenDataLossJobs)
+        {
+            lock (jobs) {
+                List<Job> jobs_copy = new List<Job> (jobs);
+                foreach (var job in jobs_copy) {
+                    if (evenDataLossJobs || !job.Has (PriorityHints.DataLossIfStopped)) {
+                        job.Cancel ();
+                    }
+                }
+            }
+        }
+
+        private void OnJobFinished (object o, EventArgs args)
+        {
+            Job job = o as Job;
+
+            lock (jobs) {
+                jobs.Remove (job);
+            }
+
+            Action<Job> handler = JobRemoved;
+            if (handler != null) {
+                handler (job);
+            }
+
+            Schedule ();
+        }
+
+        private void Schedule ()
+        {
+            lock (jobs) {
+                // First try to start any non-LongRunning jobs
+                jobs.Without (PriorityHints.LongRunning)
+                    .Where (CanStart)
+                    .ForEach (StartJob);
+
+                // Then start any LongRunning ones
+                jobs.With (PriorityHints.LongRunning)
+                    .Where (CanStart)
+                    .ForEach (StartJob);
+            }
+        }
+
+#region Job Query helpers
+
+        private bool IsRunning (Job job)
+        {
+            return job.IsRunning;
+        }
+
+        private bool CanStart (Job job)
+        {
+            return CanStartJob (job, false);
+        }
+
+        private bool CanStartJob (Job job, bool pausedJob)
+        {
+            if (!job.IsScheduled && !(pausedJob && job.IsPaused))
+                return false;
+
+            if (job.Has (PriorityHints.SpeedSensitive))
+                return true;
+            
+            // Run only one non-SpeedSensitive job that uses a given Resource
+            if (job.Has (PriorityHints.LongRunning))
+                return jobs.Where (IsRunning)
+                           .SharingResourceWith (job)
+                           .Any () == false;
+
+            // With the exception that non-LongRunning jobs will preempt LongRunning ones
+            return jobs.Where (IsRunning)
+                       .Without (PriorityHints.LongRunning)
+                       .SharingResourceWith (job)
+                       .Any () == false;
+        }
+
+        private void StartJob (Job job)
+        {
+            ConflictingJobs (job).ForEach (PreemptJob);
+            job.Start ();
+        }
+
+        private void PreemptJob (Job job)
+        {
+            job.Preempt ();
+        }
+
+        private IEnumerable<Job> ConflictingJobs (Job job)
+        {
+            if (job.Has (PriorityHints.SpeedSensitive)) {
+                // Preempt non-SpeedSensitive jobs that use the same Resource(s)
+                return jobs.Where (IsRunning)
+                           .Without (PriorityHints.SpeedSensitive)
+                           .SharingResourceWith (job);
+            } else if (!job.Has (PriorityHints.LongRunning)) {
+                // Preempt any LongRunning jobs that use the same Resource(s)
+                return jobs.Where (IsRunning)
+                           .With (PriorityHints.LongRunning)
+                           .SharingResourceWith (job);
+            }
+
+            return Enumerable.Empty<Job> ();
+        }
+
+#endregion
+
+    }
+}
diff --git a/src/Libraries/Hyena/Hyena.Jobs/SimpleAsyncJob.cs b/src/Libraries/Hyena/Hyena.Jobs/SimpleAsyncJob.cs
new file mode 100644
index 0000000..0b0df75
--- /dev/null
+++ b/src/Libraries/Hyena/Hyena.Jobs/SimpleAsyncJob.cs
@@ -0,0 +1,79 @@
+//
+// SimpleAsyncJob.cs
+//
+// Author:
+//   Gabriel Burt <gburt novell com>
+//
+// Copyright (C) 2009 Novell, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+using System.Linq;
+using System.Threading;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+
+namespace Hyena.Jobs
+{
+    public abstract class SimpleAsyncJob : Job
+    {
+        private Thread thread;
+
+        public SimpleAsyncJob ()
+        {
+        }
+
+        public SimpleAsyncJob (string name, PriorityHints hints, params Resource [] resources)
+            : base (name, hints, resources)
+        {
+        }
+
+        protected override void RunJob ()
+        {
+            if (thread == null) {
+                thread = new Thread (InnerStart);
+                thread.Name = String.Format ("Hyena.Jobs.JobRunner ({0})", Title);
+                thread.Priority = this.Has (PriorityHints.SpeedSensitive) ? ThreadPriority.Normal : ThreadPriority.Lowest;
+                thread.Start ();
+            }
+        }
+
+        protected void AbortThread ()
+        {
+            if (thread != null) {
+                thread.Abort ();
+            }
+        }
+
+        private void InnerStart ()
+        {
+            try {
+                Run ();
+            } catch (ThreadAbortException) {
+            } catch (Exception e) {
+                Log.Exception (e);
+            }
+        }
+
+        protected abstract void Run ();
+    }
+}
diff --git a/src/Libraries/Hyena/Hyena.Jobs/Tests/SchedulerTests.cs b/src/Libraries/Hyena/Hyena.Jobs/Tests/SchedulerTests.cs
new file mode 100644
index 0000000..be04ce5
--- /dev/null
+++ b/src/Libraries/Hyena/Hyena.Jobs/Tests/SchedulerTests.cs
@@ -0,0 +1,203 @@
+//
+// SchedulerTests.cs
+//
+// Author:
+//   Gabriel Burt <gburt novell com>
+//
+// Copyright (C) 2009 Novell, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#if ENABLE_TESTS
+
+using System;
+using System.Linq;
+using System.Threading;
+
+using NUnit.Framework;
+
+using Hyena;
+    
+namespace Hyena.Jobs
+{
+    [TestFixture]
+    public class SchedulerTests
+    {
+        private Scheduler scheduler;
+
+        [SetUp]
+        public void Setup ()
+        {
+            //Log.Debugging = true;
+            TestJob.job_count = 0;
+            Log.Debug ("New job scheduler test");
+        }
+
+        [TearDown]
+        public void TearDown ()
+        {
+            if (scheduler != null) {
+                // Ensure the scheduler's jobs are all finished, otherwise
+                // their job threads will be killed, throwing an exception
+                while (scheduler.JobCount > 0);
+            }
+
+            //Log.Debugging = false;
+        }
+
+        [Test]
+        public void TestSimultaneousSpeedJobs ()
+        {
+            scheduler = new Scheduler ();
+            scheduler.Add (new TestJob (200, PriorityHints.SpeedSensitive, Resource.Cpu, Resource.Disk));
+            scheduler.Add (new TestJob (200, PriorityHints.SpeedSensitive, Resource.Cpu, Resource.Disk));
+            scheduler.Add (new TestJob (200, PriorityHints.None, Resource.Cpu, Resource.Disk));
+
+            // Test that two SpeedSensitive jobs with the same Resources will run simultaneously
+            AssertJobsRunning (2);
+
+            // but that the third that isn't SpeedSensitive won't run until they are both done
+            while (scheduler.JobCount > 1);
+            Assert.AreEqual (PriorityHints.None, scheduler.Jobs.First ().PriorityHints);
+        }
+
+        [Test]
+        public void TestOneNonSpeedJobPerResource ()
+        {
+            // Test that two SpeedSensitive jobs with the same Resources will run simultaneously
+            scheduler = new Scheduler ();
+            scheduler.Add (new TestJob (200, PriorityHints.None, Resource.Cpu, Resource.Disk));
+            scheduler.Add (new TestJob (200, PriorityHints.None, Resource.Cpu, Resource.Disk));
+            AssertJobsRunning (1);
+        }
+
+        [Test]
+        public void TestSpeedJobPreemptsNonSpeedJobs ()
+        {
+            scheduler = new Scheduler ();
+            TestJob a = new TestJob (200, PriorityHints.None, Resource.Cpu);
+            TestJob b = new TestJob (200, PriorityHints.None, Resource.Disk);
+            TestJob c = new TestJob (200, PriorityHints.LongRunning, Resource.Database);
+            scheduler.Add (a);
+            scheduler.Add (b);
+            scheduler.Add (c);
+
+            // Test that three jobs got started
+            AssertJobsRunning (3);
+
+            scheduler.Add (new TestJob (200, PriorityHints.SpeedSensitive, Resource.Cpu, Resource.Disk));
+
+            // Make sure the SpeedSensitive jobs has caused the Cpu and Disk jobs to be paused
+            AssertJobsRunning (2);
+            Assert.AreEqual (true, a.IsScheduled);
+            Assert.AreEqual (true, b.IsScheduled);
+            Assert.AreEqual (true, c.IsRunning);
+        }
+
+        /*[Test]
+        public void TestManyJobs ()
+        {
+            var timer = System.Diagnostics.Stopwatch.StartNew ();
+            scheduler = new Scheduler ("TestManyJobs");
+
+            // First add some long running jobs
+            for (int i = 0; i < 100; i++) {
+                scheduler.Add (new TestJob (20, PriorityHints.LongRunning, Resource.Cpu));
+            }
+
+            // Then add some normal jobs that will prempt them
+            for (int i = 0; i < 100; i++) {
+                scheduler.Add (new TestJob (10, PriorityHints.None, Resource.Cpu));
+            }
+
+            // Then add some SpeedSensitive jobs that will prempt all of them
+            for (int i = 0; i < 100; i++) {
+                scheduler.Add (new TestJob (5, PriorityHints.SpeedSensitive, Resource.Cpu));
+            }
+
+            while (scheduler.Jobs.Count > 0);
+            Log.DebugFormat ("Took {0} to schedule and process all jobs", timer.Elapsed);
+            //scheduler.StopAll ();
+        }*/
+
+        /*[Test]
+        public void TestCannotDisposeWhileDatalossJobsScheduled ()
+        {
+            scheduler = new Scheduler ();
+            TestJob loss_job;
+            scheduler.Add (new TestJob (200, PriorityHints.SpeedSensitive, Resource.Cpu));
+            scheduler.Add (loss_job = new TestJob (200, PriorityHints.DataLossIfStopped, Resource.Cpu));
+
+            AssertJobsRunning (1);
+            Assert.AreEqual (false, scheduler.JobInfo[loss_job].IsRunning);
+
+            try {
+                //scheduler.StopAll ();
+                Assert.Fail ("Cannot stop with dataloss job scheduled");
+            } catch {
+            }
+        }
+
+        public void TestCannotDisposeWhileDatalossJobsRunning ()
+        {
+            scheduler = new Scheduler ();
+            scheduler.Add (new TestJob (200, PriorityHints.DataLossIfStopped, Resource.Cpu));
+            AssertJobsRunning (1);
+
+            try {
+                //scheduler.StopAll ();
+                Assert.Fail ("Cannot stop with dataloss job running");
+            } catch {
+            }
+        }*/
+
+        private void AssertJobsRunning (int count)
+        {
+            Assert.AreEqual (count, scheduler.Jobs.Count (j => j.IsRunning));
+        }
+
+        private class TestJob : SimpleAsyncJob
+        {
+            internal static int job_count;
+            int iteration;
+            int sleep_time;
+
+            public TestJob (int sleep_time, PriorityHints hints, params Resource [] resources)
+                : base (String.Format ("{0} ( {1}, {2})", job_count++, hints, resources.Aggregate ("", (a, b) => a += b.Id + " ")),
+                        hints,
+                        resources)
+            {
+                this.sleep_time = sleep_time;
+            }
+
+            protected override void Run ()
+            {
+                for (int i = 0; !IsCancelRequested && i < 2; i++) {
+                    YieldToScheduler ();
+                    Hyena.Log.DebugFormat ("{0} iteration {1}", Title, iteration++);
+                    System.Threading.Thread.Sleep (sleep_time);
+                }
+            }
+        }
+    }
+}
+
+#endif
diff --git a/src/Libraries/Hyena/Hyena/Timer.cs b/src/Libraries/Hyena/Hyena/Timer.cs
index 72c4f48..9f4d3f3 100644
--- a/src/Libraries/Hyena/Hyena/Timer.cs
+++ b/src/Libraries/Hyena/Hyena/Timer.cs
@@ -34,6 +34,10 @@ namespace Hyena
     {
         private DateTime start;
         private string label;
+
+        public Timer (string format, params object [] vals) : this (String.Format (format, vals))
+        {
+        }
         
         public Timer (string label) 
         {
diff --git a/src/Libraries/Hyena/Makefile.am b/src/Libraries/Hyena/Makefile.am
index b670ab8..9a4699e 100644
--- a/src/Libraries/Hyena/Makefile.am
+++ b/src/Libraries/Hyena/Makefile.am
@@ -48,6 +48,13 @@ SOURCES =  \
 	Hyena.Data/ModelSelection.cs \
 	Hyena.Data/PropertyStore.cs \
 	Hyena.Data/SortType.cs \
+	Hyena.Jobs/Job.cs \
+	Hyena.Jobs/JobExtensions.cs \
+	Hyena.Jobs/PriorityHints.cs \
+	Hyena.Jobs/Resource.cs \
+	Hyena.Jobs/Scheduler.cs \
+	Hyena.Jobs/SimpleAsyncJob.cs \
+	Hyena.Jobs/Tests/SchedulerTests.cs \
 	Hyena.Json/Deserializer.cs \
 	Hyena.Json/IJsonCollection.cs \
 	Hyena.Json/JsonArray.cs \



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]