├── .gitignore ├── Event.cs ├── Exception.cs ├── Failure.cs ├── Failures ├── Backend.cs └── Redis.cs ├── Job.cs ├── Jobs ├── BaseJob.cs ├── DirtyExitException.cs ├── DontPerformException.cs ├── IJob.cs └── Status.cs ├── LICENSE ├── Properties └── AssemblyInfo.cs ├── README.md ├── Resque.cs ├── Stat.cs ├── Worker.cs ├── csharp-resque.csproj └── packages.config /.gitignore: -------------------------------------------------------------------------------- 1 | # Build Folders (you can keep bin if you'd like, to store dlls and pdbs) 2 | [Bb]in/ 3 | [Oo]bj/ 4 | 5 | # mstest test results 6 | TestResults 7 | 8 | ## Ignore Visual Studio temporary files, build results, and 9 | ## files generated by popular Visual Studio add-ons. 10 | 11 | # User-specific files 12 | *.suo 13 | *.user 14 | *.sln.docstates 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Rr]elease/ 19 | x64/ 20 | *_i.c 21 | *_p.c 22 | *.ilk 23 | *.meta 24 | *.obj 25 | *.pch 26 | *.pdb 27 | *.pgc 28 | *.pgd 29 | *.rsp 30 | *.sbr 31 | *.tlb 32 | *.tli 33 | *.tlh 34 | *.tmp 35 | *.log 36 | *.vspscc 37 | *.vssscc 38 | .builds 39 | 40 | # Visual C++ cache files 41 | ipch/ 42 | *.aps 43 | *.ncb 44 | *.opensdf 45 | *.sdf 46 | 47 | # Visual Studio profiler 48 | *.psess 49 | *.vsp 50 | *.vspx 51 | 52 | # Guidance Automation Toolkit 53 | *.gpState 54 | 55 | # ReSharper is a .NET coding add-in 56 | _ReSharper* 57 | 58 | # NCrunch 59 | *.ncrunch* 60 | .*crunch*.local.xml 61 | 62 | # Installshield output folder 63 | [Ee]xpress 64 | 65 | # DocProject is a documentation generator add-in 66 | DocProject/buildhelp/ 67 | DocProject/Help/*.HxT 68 | DocProject/Help/*.HxC 69 | DocProject/Help/*.hhc 70 | DocProject/Help/*.hhk 71 | DocProject/Help/*.hhp 72 | DocProject/Help/Html2 73 | DocProject/Help/html 74 | 75 | # Click-Once directory 76 | publish 77 | 78 | # Publish Web Output 79 | *.Publish.xml 80 | 81 | # NuGet Packages Directory 82 | packages 83 | 84 | # Windows Azure Build Output 85 | csx 86 | *.build.csdef 87 | 88 | # Windows Store app package directory 89 | AppPackages/ 90 | 91 | # Others 92 | [Bb]in 93 | [Oo]bj 94 | sql 95 | TestResults 96 | [Tt]est[Rr]esult* 97 | *.Cache 98 | ClientBin 99 | [Ss]tyle[Cc]op.* 100 | ~$* 101 | *.dbmdl 102 | Generated_Code #added for RIA/Silverlight projects 103 | 104 | # Backup & report files from converting an old project file to a newer 105 | # Visual Studio version. Backup files are not needed, because we have git ;-) 106 | _UpgradeReport_Files/ 107 | Backup*/ 108 | UpgradeLog*.XML 109 | -------------------------------------------------------------------------------- /Event.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Newtonsoft.Json.Linq; 3 | 4 | namespace Resque 5 | { 6 | public class Event 7 | { 8 | public static event BeforePerformHandler BeforePerform = delegate { }; 9 | public static event AfterPerformHandler AfterPerform = delegate { }; 10 | public static event FailureHandler Failure = delegate { }; 11 | public static event AfterEnqueueHandler AfterEnqueue = delegate { }; 12 | 13 | public delegate void BeforePerformHandler(Job job, EventArgs eventArgs); 14 | public delegate void AfterPerformHandler(Job job, EventArgs eventArgs); 15 | public delegate void FailureHandler(Exception exception, Job job, EventArgs eventArgs); 16 | public delegate void AfterEnqueueHandler(string className, JObject arguments, string queue, EventArgs eventArgs); 17 | 18 | public static void OnBeforePerform(Job job, EventArgs eventargs) 19 | { 20 | var handler = BeforePerform; 21 | if (handler != null) handler(job, eventargs); 22 | } 23 | 24 | public static void OnAfterPerform(Job job, EventArgs eventargs) 25 | { 26 | var handler = AfterPerform; 27 | if (handler != null) handler(job, eventargs); 28 | } 29 | 30 | public static void OnFailure(Exception exception, Job job, EventArgs eventargs) 31 | { 32 | var handler = Failure; 33 | if (handler != null) handler(exception, job, eventargs); 34 | } 35 | 36 | public static void OnAfterEnqueue(string className, JObject arguments, string queue, EventArgs eventargs) 37 | { 38 | var handler = AfterEnqueue; 39 | if (handler != null) handler(className, arguments, queue, eventargs); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Exception.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Resque 4 | { 5 | class ResqueException : Exception 6 | { 7 | public ResqueException(string message) : base(message) 8 | { 9 | 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Failure.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Resque 4 | { 5 | public class Failure 6 | { 7 | private static Type _backend; 8 | 9 | public static Type Create(object payload, Exception exception, Worker worker, String queue) 10 | { 11 | Activator.CreateInstance(_backend, payload, exception, worker, queue); 12 | return _backend; 13 | } 14 | 15 | public static Type GetBackend() 16 | { 17 | return _backend ?? (_backend = typeof(Failures.Redis)); 18 | } 19 | 20 | public static void SetBackend(Type backend) 21 | { 22 | _backend = backend; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Failures/Backend.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Resque.Failures 4 | { 5 | public abstract class Backend 6 | { 7 | public Exception Exception { get; set; } 8 | public Worker Worker { get; set; } 9 | public string Queue { get; set; } 10 | public object Payload { get; set; } 11 | 12 | protected Backend() 13 | { 14 | 15 | } 16 | 17 | protected Backend(Object payload, Exception exception, Worker worker, String queue) 18 | { 19 | Exception = exception; 20 | Worker = worker; 21 | Queue = queue; 22 | Payload = payload; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Failures/Redis.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Newtonsoft.Json.Linq; 3 | 4 | namespace Resque.Failures 5 | { 6 | using Newtonsoft.Json; 7 | 8 | public class Redis : Backend 9 | { 10 | public Redis(JObject payload, Exception exception, Worker worker, string queue) 11 | { 12 | Exception = exception?.InnerException ?? exception ?? new Exception("Unkown error."); 13 | Worker = worker; 14 | Queue = queue; 15 | Payload = payload; 16 | 17 | var data = new JObject 18 | { 19 | new JProperty("failed_at", DateTime.Now), 20 | new JProperty("payload", Payload), 21 | new JProperty("exception", Exception.GetType().ToString()), 22 | new JProperty("error", Exception.Message), 23 | new JProperty("backtrace", new JArray {Exception.ToString()}), 24 | new JProperty("worker", Worker.ToString()), 25 | new JProperty("queue", Queue) 26 | }; 27 | 28 | using (var redis = Resque.PooledRedisClientManager.GetClient()) 29 | { 30 | redis.PushItemToList("resque:failed", data.ToString(Formatting.None)); 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Job.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | using System.Security.Cryptography; 5 | using Newtonsoft.Json; 6 | using Newtonsoft.Json.Linq; 7 | using Resque.Jobs; 8 | 9 | namespace Resque 10 | { 11 | public class Job 12 | { 13 | public JObject Payload { get; set; } 14 | public string Queue { get; set; } 15 | public Worker Worker { get; set; } 16 | 17 | public int? Status 18 | { 19 | get 20 | { 21 | return new Status(Payload["id"].ToString()).Get(); 22 | } 23 | } 24 | 25 | private object _instance; 26 | 27 | public static bool Create(string queue, string className, JArray args, bool monitor = false) 28 | { 29 | if (String.IsNullOrEmpty(className)) 30 | { 31 | throw new NoClassError(); 32 | } 33 | 34 | var id = BitConverter.ToString(MD5.Create().ComputeHash(new Guid().ToByteArray())).Replace("-", "").ToLower(); 35 | 36 | if (monitor) 37 | { 38 | Jobs.Status.Create(id); 39 | } 40 | 41 | var data = new JObject 42 | { 43 | new JProperty("class", className), 44 | new JProperty("args", args) 45 | }; 46 | Resque.Push(queue, data); 47 | return true; 48 | } 49 | 50 | public static Job Reserve(string queue) 51 | { 52 | var payload = Resque.Pop(queue); 53 | return payload == null ? null : new Job(queue, payload); 54 | } 55 | 56 | public Job(string queue, JObject payload) 57 | { 58 | Queue = queue; 59 | Payload = payload; 60 | } 61 | 62 | public void UpdateStatus(int status) 63 | { 64 | var payloadId = Payload["id"]; 65 | if (payloadId != null) 66 | { 67 | var statusInstance = new Status(payloadId.ToString()); 68 | statusInstance.Update(status); 69 | } 70 | } 71 | 72 | public JObject GetArgs() 73 | { 74 | return new JObject { {"Values", Payload["args"]} }; 75 | } 76 | 77 | public object GetInstance() 78 | { 79 | if (_instance != null) return _instance; 80 | 81 | var type = Resque.RegisteredJobs[Payload["class"].Value()]; 82 | 83 | if (type == null) 84 | { 85 | throw new ResqueException("Could not find job class " + Payload["class"] + "."); 86 | } 87 | 88 | _instance = Activator.CreateInstance(type); 89 | 90 | var performMethod = type.GetMethod("Perform", BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy); 91 | 92 | if (performMethod == null) 93 | { 94 | throw new ResqueException("Job class " + Payload["class"] + " does not contain a 'Perform' method."); 95 | } 96 | 97 | type.InvokeMember("Job", BindingFlags.SetProperty | BindingFlags.Instance | BindingFlags.Public, null, _instance, new object[] { this }); 98 | type.InvokeMember("Args", BindingFlags.SetProperty | BindingFlags.Instance | BindingFlags.Public, null, _instance, new object[] { GetArgs() }); 99 | type.InvokeMember("Queue", BindingFlags.SetProperty | BindingFlags.Instance | BindingFlags.Public, null, _instance, new object[] { Queue }); 100 | 101 | return _instance; 102 | } 103 | 104 | public bool Perform() 105 | { 106 | var instance = GetInstance(); 107 | 108 | var type = Resque.RegisteredJobs[Payload["class"].ToString()]; 109 | 110 | var performMethod = type.GetMethod("Perform", BindingFlags.Instance | BindingFlags.Public); 111 | var setUpMethod = type.GetMethod("SetUp", BindingFlags.Instance | BindingFlags.Public); 112 | var tearDownMethod = type.GetMethod("TearDown", BindingFlags.Instance | BindingFlags.Public); 113 | 114 | try 115 | { 116 | Event.OnBeforePerform(this, EventArgs.Empty); 117 | 118 | if (setUpMethod != null) 119 | { 120 | setUpMethod.Invoke(instance, new object[] { }); 121 | } 122 | 123 | performMethod.Invoke(instance, new object[] { }); 124 | 125 | if (tearDownMethod != null) 126 | { 127 | tearDownMethod.Invoke(instance, new object[] { }); 128 | } 129 | 130 | Event.OnAfterPerform(this, EventArgs.Empty); 131 | } 132 | catch (DontPerformException) 133 | { 134 | return false; 135 | } 136 | 137 | return true; 138 | } 139 | 140 | public void Recreate() 141 | { 142 | var status = new Status(Payload["id"].ToString()); 143 | var monitor = false; 144 | 145 | if (status.IsTracking()) 146 | { 147 | monitor = true; 148 | } 149 | 150 | Create(Queue, Payload["class"].Value(), Payload["args"].Value(), monitor); 151 | } 152 | 153 | public void Fail(Exception e) 154 | { 155 | Event.OnFailure(e, this, EventArgs.Empty); 156 | UpdateStatus(Jobs.Status.StatusFailed); 157 | new Failures.Redis(Payload, e, Worker, Queue); 158 | Stat.Increment("failed"); 159 | Stat.Increment("failed:" + Worker); 160 | } 161 | 162 | public override string ToString() 163 | { 164 | var name = new List {"Job{" + Queue + "}"}; 165 | 166 | if (Payload["id"] != null) 167 | { 168 | name.Add("ID: " + Payload["id"]); 169 | } 170 | 171 | name.Add(Payload["class"].ToString()); 172 | 173 | if (Payload["args"] != null) 174 | { 175 | name.Add(JsonConvert.SerializeObject(Payload["args"])); 176 | } 177 | return "(" + String.Join("|", name) + ")"; 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /Jobs/BaseJob.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | 3 | namespace Resque.Jobs 4 | { 5 | public abstract class BaseJob : IJob 6 | { 7 | public Job Job { get; set; } 8 | public string Queue { get; set; } 9 | public JObject Args { get; set; } 10 | 11 | public abstract void Perform(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Jobs/DirtyExitException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Resque.Jobs 4 | { 5 | class DirtyExitException : SystemException 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Jobs/DontPerformException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Resque.Jobs 4 | { 5 | class DontPerformException : Exception 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Jobs/IJob.cs: -------------------------------------------------------------------------------- 1 | namespace Resque.Jobs 2 | { 3 | public interface IJob 4 | { 5 | void Perform(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Jobs/Status.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Newtonsoft.Json; 4 | using Newtonsoft.Json.Linq; 5 | 6 | namespace Resque.Jobs 7 | { 8 | internal class Status 9 | { 10 | public const int StatusWaiting = 1; 11 | public const int StatusRunning = 2; 12 | public const int StatusFailed = 3; 13 | public const int StatusComplete = 4; 14 | 15 | private readonly string _id; 16 | private bool? _isTracking; 17 | 18 | private static readonly int[] CompleteStatuses = new[] { StatusFailed, StatusComplete }; 19 | 20 | public Status(string id) 21 | { 22 | _id = id; 23 | } 24 | 25 | public static void Create(string id) 26 | { 27 | var unixTimestamp = (long)(DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0)).TotalSeconds; 28 | 29 | var statusPacket = new JObject 30 | { 31 | new JProperty("status", StatusWaiting), 32 | new JProperty("updated", unixTimestamp), 33 | new JProperty("started", unixTimestamp) 34 | }; 35 | 36 | using (var redis = Resque.PooledRedisClientManager.GetClient()) 37 | { 38 | redis.Set("resque:job:" + id + ":status", statusPacket.ToString()); 39 | } 40 | } 41 | 42 | public bool IsTracking() 43 | { 44 | if (_isTracking == false) return false; 45 | 46 | using (var redis = Resque.PooledRedisClientManager.GetClient()) 47 | { 48 | if (redis.GetValue(ToString()) != null) 49 | { 50 | _isTracking = false; 51 | return false; 52 | } 53 | } 54 | 55 | _isTracking = true; 56 | return true; 57 | } 58 | 59 | public void Update(int status) 60 | { 61 | if (!IsTracking()) return; 62 | 63 | var unixTimestamp = (long)(DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0)).TotalSeconds; 64 | 65 | var statusPacket = new JObject 66 | { 67 | new JProperty("status", status), 68 | new JProperty("updated", unixTimestamp) 69 | }; 70 | 71 | using (var redis = Resque.PooledRedisClientManager.GetClient()) 72 | { 73 | redis.Set(ToString(), statusPacket.ToString()); 74 | 75 | if (CompleteStatuses.Contains(status)) 76 | { 77 | redis.ExpireEntryIn(ToString(), new TimeSpan(1, 0, 0, 0)); 78 | } 79 | } 80 | } 81 | 82 | public int? Get() 83 | { 84 | if (_isTracking == false) return null; 85 | 86 | try 87 | { 88 | using (var redis = Resque.PooledRedisClientManager.GetClient()) 89 | { 90 | var statusPacket = 91 | JsonConvert.DeserializeObject(redis.GetValue(ToString())); 92 | 93 | if (statusPacket == null) return null; 94 | 95 | return statusPacket["status"].Value(); 96 | } 97 | } 98 | catch (Exception) 99 | { 100 | return null; 101 | } 102 | } 103 | 104 | public void Stop() 105 | { 106 | using (var redis = Resque.PooledRedisClientManager.GetClient()) 107 | { 108 | redis.Remove(ToString()); 109 | } 110 | } 111 | 112 | public override string ToString() 113 | { 114 | return "resque:job:" + _id + ":status"; 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Alexandre Demers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("resque-sharp")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("Microsoft")] 12 | [assembly: AssemblyProduct("resque-sharp")] 13 | [assembly: AssemblyCopyright("Copyright © Microsoft 2010")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("f21e5ef0-d24a-4cd5-90bf-4428e7ea70fd")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | C# Resque 2 | ============ 3 | C# Resque is full port of Chris Boulton's [php-resque](https://github.com/chrisboulton/php-resque). This library can queue and execute jobs using Redis. It is fully compatible with php-resque as in, if a job was enqueued with php-resque, C# Resque should be able to run it without hiccups. 4 | 5 | ## Installing 6 | ### Using NuGet 7 | https://nuget.org/packages/CSharp-Resque/ 8 | 9 | ### Importing Project 10 | If you do not use NuGet to setup C# Resque, you will need the import its dependencies. Further information on how to get them are available on their respective sites. 11 | * [Newtonsoft.Json](http://json.codeplex.com/) 12 | * [ServiceStack.Redis](http://www.servicestack.net/) 13 | 14 | ## Getting Started 15 | Once you have C# Resque in your project's References, you need to do a minor setup. First, because of .NET's strictly-typed nature, you need to tell Resque about every class you will be using in your queues. For example: 16 | ```csharp 17 | Resque.Resque.AddJob("MyJob", typeof(MyJob)); 18 | ``` 19 | After, you need to setup the Redis config: 20 | ```csharp 21 | Resque.Resque.SetRedis("localhost", 6379, 0); 22 | ``` 23 | That's it. The minimum setup is done. If you want to attach events to your worker, this is the place to do them. If not, you are now ready to start the worker: 24 | ```csharp 25 | new Resque.Worker("*").Work(5); 26 | ``` 27 | 28 | ## Jobs 29 | ### Queueing Jobs 30 | Queuing jobs is pretty much like php-resque or even the original ruby Resque, call the static method `Enqueue` on the Resque object. 31 | 32 | ```csharp 33 | var arguments = new JObject { new JProperty("arg1", "value1"), new JProperty("arg2", false) }; 34 | Resque.Enqueue("object:action", "MyJob", arguments); 35 | ``` 36 | 37 | ### Declaring Jobs 38 | Declaring a job is straightforward. You create a new class that extends Resque.Jobs.BaseJob and you override the method `public void Perform`. At it's most basic form, a Job should have the minimum: 39 | 40 | ```csharp 41 | class MyJob : Resque.Jobs.BaseJob 42 | { 43 | public override void Perform() 44 | { 45 | JObject arguments = Args; // This is how you get your arguments from your job. 46 | } 47 | } 48 | ``` 49 | You can access the 3 property in your job: 50 | * `JObject Args`: The arguments of your job 51 | * `string Queue`: The name of the queue 52 | * `Job Job`: The job that is being executed 53 | 54 | Just like php-resque, c# Resque supports the `SetUp` and `TearDown` methods in your job to do job pre-processing and job post-processing. 55 | 56 | ## Attaching Events 57 | One of the nice things from Chris' php implementation of Resque is that it supports events. If your application needs to do something when a job fails for example, simply attach an event using C#'s event handlers. 58 | ```csharp 59 | Resque.Event.Failure += failureHandler; 60 | 61 | private static void failureHandler(Exception exception, Job job, EventArgs eventArgs) 62 | { 63 | Console.WriteLine(job.Queue + " failed."); 64 | } 65 | ``` 66 | 67 | Supported events are: 68 | * `Failure`: triggered when a job fails (throws an exception, for example). 69 | * `BeforePerform`: triggered when the job is ready to be executed, just before executing the optional `SetUp` method. 70 | * `AfterPerform`: triggered when the job is done executing, just after executing the optional `TearDown` method. 71 | * `AfterEnqueue`: triggered when a job is manually enqueued using `Resque.Enqueue`. 72 | -------------------------------------------------------------------------------- /Resque.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Newtonsoft.Json; 4 | using Newtonsoft.Json.Linq; 5 | using ServiceStack.Redis; 6 | 7 | namespace Resque 8 | { 9 | public class NoQueueError : Exception { } 10 | 11 | public class NoClassError : Exception { } 12 | 13 | public class Resque 14 | { 15 | private const string RESQUE_QUEUES_KEY = "resque:queues"; 16 | private const string RESQUE_QUEUE_KEY_PREFIX = "resque:queue:"; 17 | 18 | public const double Version = 1.0; 19 | public static Dictionary RegisteredJobs = new Dictionary(); 20 | public static PooledRedisClientManager PooledRedisClientManager; 21 | 22 | public static void SetRedis(string hostname = "localhost", int port = 6379, long database = 0, string password = "") 23 | { 24 | string pwPrefix = ""; 25 | if (password.Length > 0) 26 | { 27 | pwPrefix = password + "@"; 28 | } 29 | PooledRedisClientManager = new PooledRedisClientManager(new[] {pwPrefix + hostname + ":" + port}, 30 | new[] {pwPrefix + hostname + ":" + port}, database); 31 | } 32 | 33 | public static void Push(string queue, JObject item) 34 | { 35 | using (var redis = PooledRedisClientManager.GetClient()) 36 | { 37 | redis.AddItemToSet(RESQUE_QUEUES_KEY, queue); 38 | redis.PushItemToList(RESQUE_QUEUE_KEY_PREFIX + queue, item.ToString(Formatting.None)); 39 | } 40 | } 41 | 42 | public static JObject Pop(string queue) 43 | { 44 | using (var redis = PooledRedisClientManager.GetClient()) 45 | { 46 | var data = redis.RemoveStartFromList(RESQUE_QUEUE_KEY_PREFIX + queue); 47 | if (data == null) return null; 48 | return JsonConvert.DeserializeObject(data); 49 | } 50 | } 51 | 52 | public static long Size(string queue) 53 | { 54 | using (var redis = PooledRedisClientManager.GetClient()) 55 | { 56 | return redis.GetListCount(RESQUE_QUEUE_KEY_PREFIX + queue); 57 | } 58 | } 59 | 60 | public static bool Enqueue(string queue, string className, JObject arguments, bool trackStatus = false) 61 | { 62 | var argumentsArray = new JArray 63 | { 64 | arguments 65 | }; 66 | var result = Job.Create(queue, className, argumentsArray, trackStatus); 67 | 68 | if (result) 69 | { 70 | Event.OnAfterEnqueue(className, arguments, queue, EventArgs.Empty); 71 | } 72 | 73 | return result; 74 | } 75 | 76 | public static bool Enqueue(string queue, string className, JArray arguments, bool trackStatus = false) 77 | { 78 | var argumentsObject = new JObject 79 | { 80 | { "Values", arguments } 81 | }; 82 | 83 | var result = Job.Create(queue, className, arguments, trackStatus); 84 | 85 | if (result) 86 | { 87 | Event.OnAfterEnqueue(className, argumentsObject, queue, EventArgs.Empty); 88 | } 89 | 90 | return result; 91 | } 92 | 93 | public static Job Reserve(string queue) 94 | { 95 | return Job.Reserve(queue); 96 | } 97 | 98 | public static void AddJob(string className, Type type) 99 | { 100 | RegisteredJobs.Add(className, type); 101 | } 102 | 103 | public static HashSet Queues() 104 | { 105 | using (var redis = PooledRedisClientManager.GetClient()) 106 | { 107 | return redis.GetAllItemsFromSet(RESQUE_QUEUES_KEY); 108 | } 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Stat.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Resque 4 | { 5 | public class Stat 6 | { 7 | public static int Get(String stat) 8 | { 9 | using (var redis = Resque.PooledRedisClientManager.GetClient()) 10 | { 11 | return Int32.Parse(redis.GetValue("resque:stat:" + stat)); 12 | } 13 | } 14 | 15 | public static void Increment(String stat, int amt) 16 | { 17 | using (var redis = Resque.PooledRedisClientManager.GetClient()) 18 | { 19 | redis.IncrementValueBy("resque:stat:" + stat, amt); 20 | } 21 | } 22 | 23 | public static void Increment(String stat) 24 | { 25 | using (var redis = Resque.PooledRedisClientManager.GetClient()) 26 | { 27 | redis.Increment("resque:stat:" + stat, 1); 28 | } 29 | } 30 | 31 | public static void Decrement(String stat) 32 | { 33 | using (var redis = Resque.PooledRedisClientManager.GetClient()) 34 | { 35 | redis.Decrement("resque:stat:" + stat, 1); 36 | } 37 | } 38 | 39 | public static void Clear(String stat) 40 | { 41 | using (var redis = Resque.PooledRedisClientManager.GetClient()) 42 | { 43 | redis.Remove("resque:stat:" + stat); 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Worker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Threading; 6 | using Newtonsoft.Json; 7 | using Newtonsoft.Json.Linq; 8 | using Resque.Jobs; 9 | using System.Text; 10 | 11 | namespace Resque 12 | { 13 | public class Worker 14 | { 15 | public enum LogType 16 | { 17 | None, Normal, Verbose 18 | } 19 | 20 | public LogType LogLevel = LogType.Verbose; 21 | 22 | private readonly string[] _queues; 23 | private bool _shutDown; 24 | private bool _paused; 25 | private Job _currentJob; 26 | private int _maxThreads; 27 | public string Id { get; set; } 28 | 29 | public Worker(string[] queues, int maxThreads = -1) 30 | { 31 | _queues = queues; 32 | _maxThreads = maxThreads; 33 | Id = string.Format("{0}:{1}:{2}", Dns.GetHostName(), System.Diagnostics.Process.GetCurrentProcess().Id, String.Join(",", _queues)); 34 | } 35 | 36 | public Worker(string queue) : 37 | this(new[] { queue }) 38 | { 39 | } 40 | 41 | public void Work(int interval) 42 | { 43 | try 44 | { 45 | Startup(); 46 | 47 | var threads = new List(); 48 | 49 | while (true) 50 | { 51 | if (_shutDown) 52 | { 53 | break; 54 | } 55 | 56 | var jobs = new List(); 57 | 58 | if (!_paused) 59 | { 60 | Job job; 61 | do 62 | { 63 | job = Reserve(); 64 | 65 | if (job != null) 66 | { 67 | jobs.Add(job); 68 | } 69 | 70 | if (_maxThreads >= 0 && jobs.Count >= _maxThreads) 71 | { 72 | break; 73 | } 74 | } while (job != null); 75 | } 76 | 77 | if (jobs.Count == 0) 78 | { 79 | if (interval == 0) break; 80 | 81 | Log("Sleeping for " + interval * 1000); 82 | Thread.Sleep(interval * 1000); 83 | continue; 84 | } 85 | 86 | foreach (var job in jobs) 87 | { 88 | var job1 = job; 89 | 90 | ThreadStart threadStart = delegate 91 | { 92 | Log("Got " + job1.Queue); 93 | WorkingOn(job1); 94 | Perform(job1); 95 | }; 96 | var thread = new Thread(threadStart); 97 | threads.Add(thread); 98 | thread.Start(); 99 | } 100 | 101 | foreach (var thread in threads) 102 | { 103 | thread.Join(); 104 | } 105 | 106 | DoneWorking(); 107 | } 108 | } 109 | finally 110 | { 111 | UnregisterWorker(); 112 | } 113 | } 114 | 115 | public void Perform(Job job) 116 | { 117 | try 118 | { 119 | job.Perform(); 120 | } 121 | catch (Exception e) 122 | { 123 | Log(job + " failed: " + e.Message); 124 | job.Fail(e); 125 | return; 126 | } 127 | 128 | job.UpdateStatus(Status.StatusComplete); 129 | Log("Done " + job); 130 | } 131 | 132 | public Job Reserve() 133 | { 134 | foreach (var queue in Queues()) 135 | { 136 | Log("Checking " + queue); 137 | var job = global::Resque.Job.Reserve(queue); 138 | 139 | if (job == null) continue; 140 | Log("Found job on " + queue); 141 | return job; 142 | } 143 | return null; 144 | } 145 | 146 | public string[] Queues() 147 | { 148 | return _queues.Contains("*") ? FetchQueues() : _queues; 149 | } 150 | 151 | public string[] FetchQueues() 152 | { 153 | return Resque.Queues().ToArray(); 154 | } 155 | 156 | private void Startup() 157 | { 158 | RegisterWorker(); 159 | } 160 | 161 | public void PauseProcessing() 162 | { 163 | _paused = true; 164 | Log("Pausing job processing."); 165 | } 166 | 167 | public void ResumeProcessing() 168 | { 169 | _paused = true; 170 | Log("Resuming job processing."); 171 | } 172 | 173 | public void Shutdown() 174 | { 175 | _shutDown = true; 176 | Log("Exiting…"); 177 | } 178 | 179 | private void RegisterWorker() 180 | { 181 | using (var redis = Resque.PooledRedisClientManager.GetClient()) 182 | { 183 | redis.AddItemToSet("resque:workers", ToString()); 184 | redis.Set("resque:worker:" + ToString() + ":started", CurrentTimeFormatted()); 185 | } 186 | } 187 | 188 | public void UnregisterWorker() 189 | { 190 | if (_currentJob != null) 191 | { 192 | _currentJob.Fail(new DirtyExitException()); 193 | } 194 | 195 | using (var redis = Resque.PooledRedisClientManager.GetClient()) 196 | { 197 | redis.RemoveItemFromSet("resque:workers", Id); 198 | redis.Remove("resque:worker:" + Id); 199 | redis.Remove("resque:worker:" + Id + ":started"); 200 | } 201 | 202 | Stat.Clear("processed:" + Id); 203 | Stat.Clear("failed:" + Id); 204 | } 205 | 206 | public void WorkingOn(Job job) 207 | { 208 | job.Worker = this; 209 | _currentJob = job; 210 | job.UpdateStatus(Status.StatusRunning); 211 | 212 | var data = new JObject 213 | { 214 | { "queue", job.Queue }, 215 | { "run_at", CurrentTimeFormatted() }, 216 | { "payload", job.Payload } 217 | }; 218 | 219 | using (var redis = Resque.PooledRedisClientManager.GetClient()) 220 | { 221 | redis.Set("resque:worker:" + job.Worker, Encoding.ASCII.GetBytes(data.ToString(Formatting.None))); 222 | } 223 | } 224 | 225 | public void DoneWorking() 226 | { 227 | _currentJob = null; 228 | Stat.Increment("processed"); 229 | Stat.Increment("processed:" + ToString()); 230 | using (var redis = Resque.PooledRedisClientManager.GetClient()) 231 | { 232 | redis.Remove("resque:worker:" + ToString()); 233 | } 234 | } 235 | 236 | public override string ToString() 237 | { 238 | return Id; 239 | } 240 | 241 | public void Log(string message) 242 | { 243 | switch (LogLevel) 244 | { 245 | case LogType.Normal: 246 | Console.WriteLine("*** " + message); 247 | break; 248 | case LogType.Verbose: 249 | Console.WriteLine(string.Format("[{0}] {1}", DateTime.UtcNow.ToString("u"), message)); 250 | break; 251 | } 252 | } 253 | 254 | public JObject Job() 255 | { 256 | using (var redis = Resque.PooledRedisClientManager.GetClient()) 257 | { 258 | return JsonConvert.DeserializeObject(redis.GetValue("resque:worker:" + Id)); 259 | } 260 | 261 | } 262 | 263 | public int GetStat(string stat) 264 | { 265 | return Stat.Get(stat + ":" + ToString()); 266 | } 267 | 268 | private static string CurrentTimeFormatted() 269 | { 270 | return DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss zz00"); 271 | } 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /csharp-resque.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Debug 5 | AnyCPU 6 | 9.0.30729 7 | 2.0 8 | {27BE6022-29AF-4958-888A-4CF25B39ABC1} 9 | Library 10 | Properties 11 | Resque 12 | csharp-resque 13 | v4.6.1 14 | 512 15 | 16 | 17 | 3.5 18 | 19 | publish\ 20 | true 21 | Disk 22 | false 23 | Foreground 24 | 7 25 | Days 26 | false 27 | false 28 | true 29 | 0 30 | 1.0.0.%2a 31 | false 32 | false 33 | true 34 | 35 | 36 | 37 | true 38 | full 39 | false 40 | bin\Debug\ 41 | DEBUG;TRACE 42 | prompt 43 | 4 44 | AllRules.ruleset 45 | false 46 | 47 | 48 | pdbonly 49 | true 50 | bin\Release\ 51 | TRACE 52 | prompt 53 | 4 54 | AllRules.ruleset 55 | false 56 | 57 | 58 | 59 | ..\csharp_workers\packages\Newtonsoft.Json.5.0.6\lib\net45\Newtonsoft.Json.dll 60 | True 61 | 62 | 63 | ..\csharp_workers\packages\ServiceStack.Common.3.9.49\lib\net35\ServiceStack.Common.dll 64 | True 65 | 66 | 67 | ..\csharp_workers\packages\ServiceStack.Common.3.9.49\lib\net35\ServiceStack.Interfaces.dll 68 | True 69 | 70 | 71 | ..\csharp_workers\packages\ServiceStack.Redis.3.9.49\lib\net35\ServiceStack.Redis.dll 72 | True 73 | 74 | 75 | ..\csharp_workers\packages\ServiceStack.Text.3.9.49\lib\net35\ServiceStack.Text.dll 76 | True 77 | 78 | 79 | 80 | 3.5 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | False 103 | .NET Framework 3.5 SP1 Client Profile 104 | false 105 | 106 | 107 | False 108 | .NET Framework 3.5 SP1 109 | true 110 | 111 | 112 | False 113 | Windows Installer 3.1 114 | true 115 | 116 | 117 | 118 | 119 | 120 | 121 | 128 | -------------------------------------------------------------------------------- /packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | --------------------------------------------------------------------------------