Skip to content

Commit f55503c

Browse files
committed
Added Job lifecycle callbacks functionality.
1 parent 8badc22 commit f55503c

File tree

5 files changed

+779
-17
lines changed

5 files changed

+779
-17
lines changed

Models/Queue.js

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,9 @@ export class Queue {
308308

309309
/**
310310
*
311-
* Execute a job.
311+
* Process a job.
312+
*
313+
* Job lifecycle callbacks are called as appropriate throughout the job processing lifecycle.
312314
*
313315
* Job is deleted upon successful completion.
314316
*
@@ -321,25 +323,39 @@ export class Queue {
321323
*/
322324
async processJob(job) {
323325

326+
// Data must be cloned off the realm job object for several lifecycle callbacks to work correctly.
327+
// This is because realm job is deleted before some callbacks are called if job processed successfully.
328+
// More info: https://github.com/billmalarky/react-native-queue/issues/2#issuecomment-361418965
329+
const jobName = job.name;
330+
const jobId = job.id;
331+
const jobPayload = JSON.parse(job.payload);
332+
333+
// Fire onStart job lifecycle callback
334+
this.worker.executeJobLifecycleCallback('onStart', jobName, jobId, jobPayload);
335+
324336
try {
325337

326338
await this.worker.executeJob(job);
327339

328-
// On job completion, remove job
340+
// On successful job completion, remove job
329341
this.realm.write(() => {
330342

331343
this.realm.delete(job);
332344

333345
});
334346

347+
// Job has processed successfully, fire onSuccess and onComplete job lifecycle callbacks.
348+
this.worker.executeJobLifecycleCallback('onSuccess', jobName, jobId, jobPayload);
349+
this.worker.executeJobLifecycleCallback('onComplete', jobName, jobId, jobPayload);
350+
335351
} catch (error) {
336352

337353
// Handle job failure logic, including retries.
354+
let jobData = JSON.parse(job.data);
355+
338356
this.realm.write(() => {
339357

340358
// Increment failed attempts number
341-
let jobData = JSON.parse(job.data);
342-
343359
if (!jobData.failedAttempts) {
344360
jobData.failedAttempts = 1;
345361
} else {
@@ -365,6 +381,15 @@ export class Queue {
365381

366382
});
367383

384+
// Execute job onFailure lifecycle callback.
385+
this.worker.executeJobLifecycleCallback('onFailure', jobName, jobId, jobPayload);
386+
387+
// If job has failed all attempts execute job onFailed and onComplete lifecycle callbacks.
388+
if (jobData.failedAttempts >= jobData.attempts) {
389+
this.worker.executeJobLifecycleCallback('onFailed', jobName, jobId, jobPayload);
390+
this.worker.executeJobLifecycleCallback('onComplete', jobName, jobId, jobPayload);
391+
}
392+
368393
}
369394

370395
}

Models/Worker.js

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,12 @@ export default class Worker {
4141

4242
// Attach options to worker
4343
worker.options = {
44-
concurrency: options.concurrency || 1
44+
concurrency: options.concurrency || 1,
45+
onStart: options.onStart || null,
46+
onSuccess: options.onSuccess || null,
47+
onFailure: options.onFailure || null,
48+
onFailed: options.onFailed || null,
49+
onComplete: options.onComplete || null
4550
};
4651

4752
Worker.workers[jobName] = worker;
@@ -119,4 +124,35 @@ export default class Worker {
119124

120125
}
121126

127+
/**
128+
*
129+
* Execute an asynchronous job lifecycle callback associated with related worker.
130+
*
131+
* @param callbackName {string} - Job lifecycle callback name.
132+
* @param jobName {string} - Name associated with jobs assigned to related worker.
133+
* @param jobId {string} - Unique id associated with job.
134+
* @param jobPayload {object} - Data payload associated with job.
135+
*/
136+
async executeJobLifecycleCallback(callbackName, jobName, jobId, jobPayload) {
137+
138+
// Validate callback name
139+
const validCallbacks = ['onStart', 'onSuccess', 'onFailure', 'onFailed', 'onComplete'];
140+
if (!validCallbacks.includes(callbackName)) {
141+
throw new Error('Invalid job lifecycle callback name.');
142+
}
143+
144+
// Fire job lifecycle callback if set.
145+
// Uses a try catch statement to gracefully degrade errors in production.
146+
if (Worker.workers[jobName].options[callbackName]) {
147+
148+
try {
149+
await Worker.workers[jobName].options[callbackName](jobId, jobPayload);
150+
} catch (error) {
151+
console.error(error); // eslint-disable-line no-console
152+
}
153+
154+
}
155+
156+
}
157+
122158
}

README.md

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ A React Native at-least-once priority job queue / task queue backed by persisten
1717
* [Example Use Cases](#example-use-cases)
1818
* [Installation](#installation)
1919
* [Basic Usage](#basic-usage)
20-
* [Options](#options)
20+
* [Options and Job Lifecycle Callbacks](#options-and-job-lifecycle-callbacks)
2121
* [Testing with Jest](#testing-with-jest)
2222
* [Caveats](#caveats)
2323
* [Advanced Usage Examples](#advanced-usage-examples)
@@ -30,7 +30,7 @@ A React Native at-least-once priority job queue / task queue backed by persisten
3030
* **Simple API:** Set up job workers and begin creating your jobs in minutes with just two basic API calls
3131
* queue.addWorker(name, workerFunction, options = {})
3232
* queue.createJob(name, payload = {}, options = {}, startQueue = true)
33-
* **Powerful options:** Easily modify default functionality. Set job timeouts, number of retry attempts, priority, and worker concurrency with an options object. Start queue processing with a lifespan to easily meet OS background task time limits.
33+
* **Powerful options:** Easily modify default functionality. Set job timeouts, number of retry attempts, priority, job lifecycle callbacks, and worker concurrency with an options object. Start queue processing with a lifespan to easily meet OS background task time limits.
3434
* **Persistent Jobs:** Jobs are persisted with Realm. Because jobs persist, you can easily continue to process jobs across app restarts or in OS background tasks until completed or failed (or app is uninstalled).
3535
* **Powerful Integrations:** React Native Queue was designed to play well with others. The queue quickly integrates with a variety of OS background task and Worker packages so processing your jobs in a background service or dedicated thread have never been easier.
3636

@@ -147,19 +147,63 @@ console.log('The above jobs are processing in the background of app now.');
147147

148148
```
149149

150-
## Options
150+
## Options and Job Lifecycle Callbacks
151151

152-
#### Worker Options
152+
#### Worker Options (includes async job lifecycle callbacks)
153153

154-
queue.addWorker() accepts an options object in order to tweak standard functionality.
154+
queue.addWorker() accepts an options object in order to tweak standard functionality and allow you to hook into asynchronous job lifecycle callbacks.
155+
156+
**IMPORTANT: Job Lifecycle callbacks are called asynchronously.** They do not block job processing or each other. Don't put logic in onStart that you expect to be completed before the actual job process begins executing. Don't put logic in onFailure you expect to be completed before onFailed is called. You can, of course, assume that the job process has completed (or failed) before onSuccess, onFailure, onFailed, or onComplete are asynchonrously called.
155157

156158
```js
157159

158-
queue.addWorker('job-name-here', (id, payload) => { console.log(id); }, {
160+
queue.addWorker('job-name-here', async (id, payload) => { console.log(id); }, {
159161

160162
// Set max number of jobs for this worker to process concurrently.
161163
// Defaults to 1.
162-
concurrency: 5
164+
concurrency: 5,
165+
166+
// JOB LIFECYCLE CALLBACKS
167+
168+
// onStart job callback handler is fired when a job begins processing.
169+
//
170+
// IMPORTANT: Job lifecycle callbacks are executed asynchronously and do not block job processing
171+
// (even if the callback returns a promise it will not be "awaited" on).
172+
// As such, do not place any logic in onStart that your actual job worker function will depend on,
173+
// this type of logic should of course go inside the job worker function itself.
174+
onStart: async (id, payload) => {
175+
176+
console.log('Job "job-name-here" with id ' + id + ' has started processing.');
177+
178+
},
179+
180+
// onSuccess job callback handler is fired after a job successfully completes processing.
181+
onSuccess: async (id, payload) => {
182+
183+
console.log('Job "job-name-here" with id ' + id + ' was successful.');
184+
185+
},
186+
187+
// onFailure job callback handler is fired after each time a job fails (onFailed also fires if job has reached max number of attempts).
188+
onFailure: async (id, payload) => {
189+
190+
console.log('Job "job-name-here" with id ' + id + ' had an attempt end in failure.');
191+
192+
},
193+
194+
// onFailed job callback handler is fired if job fails enough times to reach max number of attempts.
195+
onFailed: async (id, payload) => {
196+
197+
console.log('Job "job-name-here" with id ' + id + ' has failed.');
198+
199+
},
200+
201+
// onComplete job callback handler fires after job has completed processing successfully or failed entirely.
202+
onComplete: async (id, payload) => {
203+
204+
console.log('Job "job-name-here" with id ' + id + ' has completed processing.');
205+
206+
}
163207

164208
});
165209

0 commit comments

Comments
 (0)