├── jobs.js ├── main.js ├── README.md └── parse-rita.js /jobs.js: -------------------------------------------------------------------------------- 1 | var rita = require('cloud/parse-rita'); 2 | 3 | rita.job('hello', function(scalarArgs, objectArgs) { 4 | console.log('Hello!'); 5 | return Parse.Promise.as('Hi!'); 6 | }); 7 | 8 | rita.job('add', function(scalarArgs, objectArgs) { 9 | var sum = scalarArgs[0] + scalarArgs[1]; 10 | return Parse.Promise.as(sum); 11 | }); 12 | 13 | rita.job('updateCount', function(scalarArgs, objectArgs) { 14 | var object = objectArgs[0]; 15 | var field = scalarArgs[0]; 16 | var amount = scalarArgs[1]; 17 | object.increment(field, amount); 18 | return object.save(); 19 | }); 20 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | 2 | var rita = require('cloud/parse-rita'); 3 | 4 | require('cloud/jobs'); 5 | 6 | Parse.Cloud.define('makeTestData', function(req, res) { 7 | 8 | var obj = new Parse.Object('TestObject'); 9 | obj.set('count', 1); 10 | obj.save().then(function(obj) { 11 | promises = []; 12 | promises.push(rita.enqueue('test', 'hello')); 13 | promises.push(rita.enqueue('test', 'add', [1,2])); 14 | promises.push(rita.enqueue('test', 'updateCount', ['count', 1], [obj])); 15 | return Parse.Promise.when(promises); 16 | }).then(function() { 17 | res.success(); 18 | }, function(err) { 19 | res.error(); 20 | }); 21 | 22 | }); 23 | 24 | Parse.Cloud.job('workertest1', function(request, status) { 25 | rita.worker(request.params.queues); 26 | }); 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### ParseRita 2 | 3 | Rita, the Parse Cloud Code job assistant. She defines, queues, and processes work on a constant basis. 4 | 5 | #### USAGE 6 | 7 | First, you'll want to define some jobs: 8 | 9 | ```javascript 10 | var rita = require('cloud/parse-rita'); 11 | 12 | rita.job('hello', function(scalarArgs, objectArgs) { 13 | return Parse.Promise.as('Hi!'); 14 | }); 15 | 16 | rita.job('add', function(scalarArgs, objectArgs) { 17 | var sum = scalarArgs[0] + scalarArgs[1]; 18 | return Parse.Promise.as(sum); 19 | }); 20 | 21 | rita.job('updateCount', function(scalarArgs, objectArgs) { 22 | var object = objectArgs[0]; 23 | var field = scalarArgs[0]; 24 | var amount = scalarArgs[1]; 25 | object.increment(field, amount); 26 | object.save(); 27 | }); 28 | ``` 29 | 30 | Then, queue up a few jobs, providing a queue name, job name, an array of scalar arguments, and an array of Parse Objects. These arguments will be available to the job, and the passed in Parse Objects will be included. 31 | 32 | ```javascript 33 | var rita = require('cloud/parse-rita'); 34 | 35 | rita.enqueue('test', 'hello'); 36 | rita.enqueue('test', 'add', [1,2]); 37 | rita.enqueue('test', 'updateCount', ['countField', 1], [object]); 38 | ``` 39 | 40 | Next, define a Background Job that will run your worker: 41 | 42 | ```javascript 43 | Parse.Cloud.job('testworker', function(request, status) { 44 | rita.worker(['test']); 45 | }); 46 | ``` 47 | 48 | By default, this will process jobs for 4 minutes and 30 seconds and pause for 15 seconds after clearing the job queue. You can alter the defaults with values in milliseconds: 49 | 50 | ```javascript 51 | rita.setDelayOnEmptyQueue(15000); 52 | rita.setRunLimit(4.5 * 60 * 1000); 53 | ``` 54 | 55 | With this module you can attain a finer granularity than simpler job systems running at every-minute intervals. -------------------------------------------------------------------------------- /parse-rita.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ParseRita 3 | * 4 | * An implementation of Resque for Parse Cloud Code 5 | * 6 | * Uses background jobs as workers, and Parse Data classes for storage 7 | * 8 | * Author: Fosco Marotto 9 | */ 10 | 11 | var ResqueQueue = Parse.Object.extend("ResqueQueue"); 12 | var ResqueLog = Parse.Object.extend("ResqueLog"); 13 | 14 | var jobs = []; 15 | var queues = []; 16 | var startTime; 17 | var delayTime = 15000; // default 15s 18 | var runTime = 14.5 * 60 * 1000; // default 14m30s 19 | 20 | var setDelayOnEmptyQueue = function(timeInMs) { 21 | delayTime = timeInMs; 22 | }; 23 | 24 | var setRunLimit = function(timeInMs) { 25 | runTime = timeInMs; 26 | }; 27 | 28 | var job = function(jobName, func) { 29 | jobs[jobName] = func; 30 | }; 31 | 32 | var enqueue = function(queue, jobName, scalarArgs, objectArgs) { 33 | if (!queue || !jobName) { 34 | return log( 35 | 'Failed to queue invalid job, missing jobName or queue', 36 | { 37 | 'queue' : queue, 38 | 'jobName' : jobName, 39 | 'scalarArgs' : (scalarArgs ? scalarArgs : []), 40 | 'objectArgs' : (objectArgs ? objectArgs : []) 41 | } 42 | ); 43 | } 44 | var job = new ResqueQueue(); 45 | job.set('queue', queue); 46 | job.set('jobName', jobName); 47 | job.set('scalarArgs', scalarArgs); 48 | job.set('objectArgs', objectArgs); 49 | job.set('processed', 0); 50 | job.set('status', 'new'); 51 | job.set('result', ''); 52 | return job.save(null, { useMasterKey : true }).then(null, createHandler()); 53 | }; 54 | 55 | var worker = function(queuesParam) { 56 | startTime = Date.now(); 57 | queues = queuesParam; 58 | 59 | if (!queues || !queues.length) { 60 | return log('Failed to start worker, queues not provided', {}).then(null, createHandler()); 61 | } 62 | return log( 63 | 'Worker started at ' + startTime, 64 | { 65 | 'queues' : queues 66 | } 67 | ).then(poll, createHandler()); 68 | }; 69 | 70 | var poll = function() { 71 | return timeLimitCheck().then(function() { 72 | var query = new Parse.Query(ResqueQueue); 73 | query.containedIn('queue', queues); 74 | query.equalTo('processed', 0); 75 | query.include('objectArgs'); 76 | var jobCount = 0; 77 | return query.each(function(job) { 78 | jobCount++; 79 | return perform(job).then(function() { 80 | return Parse.Promise.as(); 81 | }, function(err) { 82 | log( 83 | 'Failed to perform job ' + job.id, { error: err } 84 | ).then(function() { 85 | return Parse.Promise.as(); 86 | }); 87 | }); 88 | }).then(function() { 89 | if (jobCount) { 90 | return log( 91 | 'Worker cycle completed after ' + jobCount + ' jobs processed', 92 | { jobCount : jobCount } 93 | ); 94 | } else { 95 | return Parse.Promise.as(); 96 | } 97 | }); 98 | }).then(delay).then(poll); 99 | }; 100 | 101 | var perform = function(job) { 102 | job.increment('processed'); 103 | return job.save(null, { useMasterKey : true }).then(function(job) { 104 | if (job.get('processed') != 1) { 105 | return Parse.Promise.as(); 106 | } 107 | var jobName = job.get('jobName'); 108 | if (!jobs[jobName]) { 109 | return log( 110 | 'Undefined jobName, ' + jobName, 111 | { job : job } 112 | ).then(function() { 113 | return job.save({ 114 | status : 'error', 115 | result : 'Undefined jobName' 116 | }, { useMasterKey : true }); 117 | }); 118 | } 119 | 120 | var scalarArgs = job.get('scalarArgs'); 121 | var objectArgs = job.get('objectArgs'); 122 | return jobs[jobName](scalarArgs, objectArgs).then(function (result) { 123 | // job completed 124 | return job.save({ 125 | 'status' : 'completed', 126 | 'result' : JSON.stringify(result) 127 | }, { useMasterKey : true }); 128 | }, function (error) { 129 | // job failed 130 | return job.save({ 131 | 'status' : 'failed', 132 | 'result' : JSON.stringify(error) 133 | }, { useMasterKey : true }); 134 | }).then(timeLimitCheck); 135 | }); 136 | }; 137 | 138 | var log = function(message, data) { 139 | var entry = new ResqueLog(); 140 | entry.set('message', message); 141 | entry.set('data', data); 142 | return entry.save(null, {useMasterKey:true}); 143 | }; 144 | 145 | var delay = function() { 146 | var delayUntil = Date.now() + delayTime; 147 | var delayPromise = new Parse.Promise(); 148 | 149 | var _delay = function () { 150 | if (Date.now() > delayUntil) { 151 | delayPromise.resolve(); 152 | return; 153 | } 154 | process.nextTick(_delay); 155 | }; 156 | _delay(); 157 | 158 | return delayPromise; 159 | }; 160 | 161 | function timeLimitCheck() { 162 | if (Date.now() > (startTime + runTime)) { 163 | return log( 164 | 'Worker closing at end of time limit.', { 165 | startTime: startTime, 166 | tunTime: runTime, 167 | time: Date.now() 168 | } 169 | ).then(terminate, createHandler()); 170 | } 171 | return Parse.Promise.as(); 172 | } 173 | 174 | function terminate() { 175 | process.abort(); 176 | } 177 | 178 | function createHandler(response) { 179 | /* 180 | * Default error handling behaviour for async requests 181 | */ 182 | function errorHandler(result) { 183 | if (result && result.message) { 184 | var msg = 'Rita error: ' + result.message; 185 | console.error(msg); 186 | // afterSave doesnt have the response object available 187 | if (response) { 188 | response.error(msg); 189 | } 190 | } 191 | } 192 | return errorHandler; 193 | } 194 | 195 | module.exports = { 196 | job : job, 197 | enqueue : enqueue, 198 | worker : worker, 199 | setDelayOnEmptyQueue : setDelayOnEmptyQueue, 200 | setRunLimit : setRunLimit 201 | }; --------------------------------------------------------------------------------