Skip to content

Commit 122dd15

Browse files
committed
feat: add way to bind process to scope
1 parent 787c121 commit 122dd15

11 files changed

Lines changed: 779 additions & 104 deletions

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@athenna/queue",
3-
"version": "5.29.0",
3+
"version": "5.30.0",
44
"description": "The Athenna queue handler.",
55
"license": "MIT",
66
"author": "João Lenon <lenon@athenna.io>",

src/drivers/AwsSqsDriver.ts

Lines changed: 47 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { Driver } from '#src/drivers/Driver'
2222
import { Is, Options, Uuid } from '@athenna/common'
2323
import type { ConnectionOptions } from '#src/types'
2424
import { ConnectionFactory } from '#src/factories/ConnectionFactory'
25+
import { QueueExecutionScope } from '#src/worker/QueueExecutionScope'
2526
import { AwsSqsDriverExceptionHandler } from '#src/handlers/AwsSqsDriverExceptionHandler'
2627
import { NotFifoSqsQueueTypeException } from '#src/exceptions/NotFifoSqsQueueTypeException'
2728

@@ -390,17 +391,30 @@ export class AwsSqsDriver extends Driver<SQSClient> {
390391
const heartbeatDelay = this.calculateHeartbeatDelay()
391392

392393
let heartbeatTimeout: NodeJS.Timeout
394+
let scope: QueueExecutionScope | null = null
393395

394396
const startHeartbeat = () => {
395397
if (heartbeatDelay <= 0) {
396398
return
397399
}
398400

399-
heartbeatTimeout = setInterval(() => {
400-
this.changeJobVisibility(
401+
const heartbeat = scope?.bind(async () => {
402+
await this.changeJobVisibility(
401403
job.id,
402404
this.msToS(this.visibilityTimeout)
403-
).catch(() => {})
405+
)
406+
})
407+
408+
heartbeatTimeout = setInterval(() => {
409+
const changeVisibility =
410+
heartbeat ||
411+
(() =>
412+
this.changeJobVisibility(
413+
job.id,
414+
this.msToS(this.visibilityTimeout)
415+
))
416+
417+
Promise.resolve(changeVisibility()).catch(() => {})
404418
}, heartbeatDelay)
405419
}
406420

@@ -419,34 +433,41 @@ export class AwsSqsDriver extends Driver<SQSClient> {
419433
data: job.data
420434
}
421435

422-
await this.runScopedQueueProcessor(processor, workerJob, async () => {
423-
try {
424-
startHeartbeat()
425-
426-
await processor(workerJob)
427-
428-
stopHeartbeat()
429-
430-
if (!AwsSqsDriver.ackedIds.has(job.id)) {
431-
await this.changeJobVisibility(
432-
job.id,
433-
this.msToS(this.noAckDelayMs + requeueJitterMs)
434-
)
436+
await this.runScopedQueueProcessor(
437+
processor,
438+
workerJob,
439+
async () => {
440+
try {
441+
startHeartbeat()
442+
443+
await processor(workerJob)
444+
445+
stopHeartbeat()
446+
447+
if (!AwsSqsDriver.ackedIds.has(job.id)) {
448+
await this.changeJobVisibility(
449+
job.id,
450+
this.msToS(this.noAckDelayMs + requeueJitterMs)
451+
)
452+
}
453+
} catch (error) {
454+
await new AwsSqsDriverExceptionHandler().handle({
455+
job,
456+
error,
457+
driver: this,
458+
stopHeartbeat,
459+
requeueJitterMs
460+
})
435461
}
436-
} catch (error) {
437-
await new AwsSqsDriverExceptionHandler().handle({
438-
job,
439-
error,
440-
driver: this,
441-
stopHeartbeat,
442-
requeueJitterMs
443-
})
462+
},
463+
executionScope => {
464+
scope = executionScope
444465
}
445-
})
466+
)
446467
}
447468

448469
/**
449-
* Send a job to the deadletter quue.
470+
* Send a job to the deadletter queue.
450471
*/
451472
public async sendJobToDLQ(job: any) {
452473
if (Is.Object(job.data)) {

src/drivers/Driver.ts

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,22 @@
88
*/
99

1010
import { Utils } from '#src/utils'
11+
import { Is } from '@athenna/common'
1112
import { Config } from '@athenna/config'
12-
import type { ConnectionOptions } from '#src/types'
13+
import type { Job, ConnectionOptions } from '#src/types'
14+
import { QueueExecutionScope } from '#src/worker/QueueExecutionScope'
1315

1416
export const RUN_WITH_WORKER_CONTEXT = Symbol.for(
1517
'@athenna/queue.runWithWorkerContext'
1618
)
1719

18-
export type ScopedQueueProcessor<T = unknown> = ((data: T) => any | Promise<any>) & {
20+
export type ScopedQueueProcessor<T = unknown> = ((
21+
data: T
22+
) => any | Promise<any>) & {
1923
[RUN_WITH_WORKER_CONTEXT]?: (
2024
data: T,
21-
callback: () => any | Promise<any>
25+
callback: () => any | Promise<any>,
26+
captureScope?: (scope: QueueExecutionScope<T>) => void
2227
) => any | Promise<any>
2328
}
2429

@@ -82,6 +87,11 @@ export abstract class Driver<Client = any> {
8287
jitter: number
8388
}
8489

90+
/**
91+
* Set the custom options used when creating this driver.
92+
*/
93+
public options?: ConnectionOptions['options']
94+
8595
/**
8696
* Creates a new instance of the Driver.
8797
*/
@@ -92,6 +102,8 @@ export abstract class Driver<Client = any> {
92102
) {
93103
const config = Config.get(`queue.connections.${connection}`)
94104

105+
this.options = options
106+
95107
this.workerInterval =
96108
options?.workerInterval || config.workerInterval || 1000
97109
this.noAckDelayMs = Utils.computeNoAckDelayMs(
@@ -178,15 +190,48 @@ export abstract class Driver<Client = any> {
178190
protected runScopedQueueProcessor<T>(
179191
processor: ScopedQueueProcessor<T>,
180192
data: T,
181-
callback: () => any | Promise<any>
193+
callback: () => any | Promise<any>,
194+
captureScope?: (scope: QueueExecutionScope<T>) => void
182195
) {
183196
const runner = processor[RUN_WITH_WORKER_CONTEXT]
184197

185198
if (runner) {
186-
return runner(data, callback)
199+
return runner(data, callback, captureScope)
200+
}
201+
202+
const scope = new QueueExecutionScope<T>({
203+
name: this.queueName,
204+
connection: this.connection,
205+
options: this.options,
206+
traceId: null,
207+
job: this.createContextJob(data)
208+
})
209+
210+
captureScope?.(scope)
211+
212+
return scope.run(callback)
213+
}
214+
215+
private createContextJob<T>(data: T) {
216+
if (this.isJob(data)) {
217+
return data
187218
}
188219

189-
return callback()
220+
return {
221+
id: null,
222+
attempts: this.attempts,
223+
data
224+
} as Job
225+
}
226+
227+
private isJob(data: unknown): data is Job {
228+
if (!data || !Is.Object(data)) {
229+
return false
230+
}
231+
232+
const candidate = data as Partial<Job>
233+
234+
return 'data' in candidate && 'attempts' in candidate
190235
}
191236

192237
/**
@@ -244,6 +289,20 @@ export abstract class Driver<Client = any> {
244289
*/
245290
public abstract isEmpty(): Promise<boolean>
246291

292+
/**
293+
* Change the job visibility values in the queue.
294+
*/
295+
public changeJobVisibility(_jobId: string, _seconds: number): Promise<void> {
296+
return Promise.resolve()
297+
}
298+
299+
/**
300+
* Send a job to the deadletter queue.
301+
*/
302+
public sendJobToDLQ(_jobId: string): Promise<void> {
303+
return Promise.resolve()
304+
}
305+
247306
/**
248307
* Process the next data in the queue.
249308
*/

src/drivers/FakeDriver.ts

Lines changed: 78 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,14 @@
88
*/
99

1010
import { Log } from '@athenna/logger'
11-
import { Json, Options } from '@athenna/common'
12-
import type { ConnectionOptions } from '#src/types'
11+
import { Is, Json, Options } from '@athenna/common'
12+
import type { Job, ConnectionOptions } from '#src/types'
1313
import { ConnectionFactory } from '#src/factories/ConnectionFactory'
14-
import { RUN_WITH_WORKER_CONTEXT, type ScopedQueueProcessor } from '#src/drivers/Driver'
14+
import { QueueExecutionScope } from '#src/worker/QueueExecutionScope'
15+
import {
16+
RUN_WITH_WORKER_CONTEXT,
17+
type ScopedQueueProcessor
18+
} from '#src/drivers/Driver'
1519

1620
export class FakeDriver {
1721
public constructor(connection?: string, client?: any) {
@@ -38,6 +42,7 @@ export class FakeDriver {
3842
public static visibilityTimeout: number = 30000
3943
public static workerInterval: number = 1000
4044
public static noAckDelayMs: number = 1700
45+
public static options?: ConnectionOptions['options']
4146
public static backoff: {
4247
type: 'fixed' | 'exponential'
4348
delay: number
@@ -99,6 +104,7 @@ export class FakeDriver {
99104

100105
this.isConnected = true
101106
this.isSavedOnFactory = options.saveOnFactory
107+
this.options = options.options
102108
}
103109

104110
/**
@@ -225,18 +231,65 @@ export class FakeDriver {
225231
return 0
226232
}
227233

228-
protected runScopedQueueProcessor<T>(
234+
/**
235+
* Change the job visibility values in the queue.
236+
*/
237+
public static changeJobVisibility(_jobId: string, _seconds: number): Promise<void> {
238+
return Promise.resolve()
239+
}
240+
241+
/**
242+
* Send a job to the deadletter queue.
243+
*/
244+
public static sendJobToDLQ(_jobId: string): Promise<void> {
245+
return Promise.resolve()
246+
}
247+
248+
private static createContextJob<T>(data: T) {
249+
if (this.isJob(data)) {
250+
return data
251+
}
252+
253+
return {
254+
id: null,
255+
attempts: this.attempts,
256+
data
257+
} as Job
258+
}
259+
260+
private static isJob(data: unknown): data is Job {
261+
if (!data || !Is.Object(data)) {
262+
return false
263+
}
264+
265+
const candidate = data as Partial<Job>
266+
267+
return 'data' in candidate && 'attempts' in candidate
268+
}
269+
270+
protected static runScopedQueueProcessor<T>(
229271
processor: ScopedQueueProcessor<T>,
230272
data: T,
231-
callback: () => any | Promise<any>
273+
callback: () => any | Promise<any>,
274+
captureScope?: (scope: QueueExecutionScope<T>) => void
232275
) {
233276
const runner = processor[RUN_WITH_WORKER_CONTEXT]
234277

235278
if (runner) {
236-
return runner(data, callback)
279+
return runner(data, callback, captureScope)
237280
}
238281

239-
return callback()
282+
const scope = new QueueExecutionScope<T>({
283+
name: this.queueName,
284+
connection: this.connection,
285+
options: this.options,
286+
traceId: null,
287+
job: this.createContextJob(data)
288+
})
289+
290+
captureScope?.(scope)
291+
292+
return scope.run(callback)
240293
}
241294

242295
/**
@@ -256,21 +309,23 @@ export class FakeDriver {
256309
) {
257310
const data = await this.pop()
258311

259-
try {
260-
await processor(data)
261-
} catch (err) {
262-
Log.channelOrVanilla('exception').error({
263-
msg: `failed to process job: ${err.message}`,
264-
queue: this.queueName,
265-
deadletter: this.deadletter,
266-
name: err.name,
267-
code: err.code,
268-
help: err.help,
269-
details: err.details,
270-
metadata: err.metadata,
271-
stack: err.stack,
272-
job: data
273-
})
274-
}
312+
await this.runScopedQueueProcessor(processor, data, async () => {
313+
try {
314+
await processor(data)
315+
} catch (err) {
316+
Log.channelOrVanilla('exception').error({
317+
msg: `failed to process job: ${err.message}`,
318+
queue: this.queueName,
319+
deadletter: this.deadletter,
320+
name: err.name,
321+
code: err.code,
322+
help: err.help,
323+
details: err.details,
324+
metadata: err.metadata,
325+
stack: err.stack,
326+
job: data
327+
})
328+
}
329+
})
275330
}
276331
}

0 commit comments

Comments
 (0)