Skip to content

Commit db9084d

Browse files
authored
Merge pull request #635 from timgit/offwork-wait
stop() async resource cleanup
2 parents a322ffc + 759e5ce commit db9084d

16 files changed

Lines changed: 331 additions & 561 deletions

docs/api/workers.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,20 @@ await boss.work(queue, async ([ job ]) => {
8383
Notifies a worker by id to bypass the job polling interval (see `pollingIntervalSeconds`) for this iteration in the loop.
8484

8585

86-
### `offWork(value)`
86+
### `offWork(name, options)`
8787

8888
Removes a worker by name or id and stops polling.
8989

9090
** Arguments **
91-
- value: string or object
91+
- name: string
92+
- options: object
9293

93-
If a string, removes all workers found matching the name. If an object, only the worker with a matching `id` will be removed.
94+
**Options**
95+
96+
* **wait**, boolean, *(default=true)*
97+
98+
If the promise should wait until current jobs finish
99+
100+
* **id**, string
101+
102+
Only stop polling by worker id
File renamed without changes.

package-lock.json

Lines changed: 240 additions & 510 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "pg-boss",
3-
"version": "12.1.1",
3+
"version": "12.2.0",
44
"description": "Queueing jobs in Postgres from Node.js like a boss",
55
"type": "module",
66
"main": "./dist/index.mjs",
@@ -20,17 +20,17 @@
2020
},
2121
"devDependencies": {
2222
"@istanbuljs/nyc-config-typescript": "^1.0.2",
23-
"@tsconfig/node22": "^22.0.2",
23+
"@tsconfig/node22": "^22.0.3",
2424
"@types/mocha": "^10.0.10",
25-
"@types/node": "^22.19.0",
25+
"@types/node": "^22.19.1",
2626
"@types/pg": "^8.15.6",
2727
"eslint": "^9.39.1",
2828
"luxon": "^3.7.2",
2929
"mocha": "^11.7.5",
3030
"neostandard": "^0.12.2",
3131
"nyc": "^17.1.0",
3232
"source-map-support": "^0.5.21",
33-
"tsdown": "^0.16.0",
33+
"tsdown": "^0.16.4",
3434
"tsx": "^4.20.6",
3535
"typescript": "^5.9.3"
3636
},

src/attorney.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ function checkWorkArgs (name: string, args: any[]): {
7979
} {
8080
let options, callback
8181

82-
assert(name, 'missing job name')
82+
assert(name, 'queue name is required')
8383

8484
if (args.length === 1) {
8585
callback = args[0]

src/db.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class Db extends EventEmitter implements types.IDatabase, types.EventsMixin {
1414
super()
1515

1616
config.application_name = config.application_name || 'pgboss'
17+
config.connectionTimeoutMillis = config.connectionTimeoutMillis || 10000
1718
// config.maxUses = config.maxUses || 1000
1819

1920
this.config = config

src/index.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,9 @@ export class PgBoss extends EventEmitter<types.PgBossEventMap> {
137137

138138
if (this.#db._pgbdb && this.#db.opened && close) {
139139
await this.#db.close()
140+
141+
// Give event loop time to process socket closes
142+
await delay(10)
140143
}
141144

142145
this.#stopped = true
@@ -150,9 +153,7 @@ export class PgBoss extends EventEmitter<types.PgBossEventMap> {
150153
return await shutdown()
151154
}
152155

153-
const isWip = () => this.#manager.getWipData({ includeInternal: false }).length > 0
154-
155-
while ((Date.now() - this.#stoppingOn!) < timeout && isWip()) {
156+
while ((Date.now() - this.#stoppingOn!) < timeout && this.#manager.hasPendingCleanups()) {
156157
await delay(500)
157158
}
158159

@@ -197,14 +198,12 @@ export class PgBoss extends EventEmitter<types.PgBossEventMap> {
197198
return this.#manager.work(...args as Parameters<Manager['work']>)
198199
}
199200

200-
offWork (name: string): Promise<void>
201-
offWork (options: types.OffWorkOptions): Promise<void>
202-
offWork (value: string | types.OffWorkOptions): Promise<void> {
203-
return this.#manager.offWork(value)
201+
offWork (name: string, options?: types.OffWorkOptions): Promise<void> {
202+
return this.#manager.offWork(name, options)
204203
}
205204

206205
notifyWorker (workerId: string): void {
207-
this.#manager.notifyWorker(workerId)
206+
return this.#manager.notifyWorker(workerId)
208207
}
209208

210209
subscribe (event: string, name: string): Promise<void> {

src/manager.ts

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class Manager extends EventEmitter implements types.EventsMixin {
2828
queueCacheInterval: NodeJS.Timeout | undefined
2929
timekeeper: Timekeeper | undefined
3030
queues: Record<string, types.QueueResult> | null
31+
pendingOffWorkCleanups: Set<Promise<void>>
3132

3233
constructor (db: types.IDatabase, config: types.ResolvedConstructorOptions) {
3334
super()
@@ -37,6 +38,7 @@ class Manager extends EventEmitter implements types.EventsMixin {
3738
this.wipTs = Date.now()
3839
this.workers = new Map()
3940
this.queues = null
41+
this.pendingOffWorkCleanups = new Set()
4042
}
4143

4244
async start () {
@@ -80,11 +82,11 @@ class Manager extends EventEmitter implements types.EventsMixin {
8082

8183
clearInterval(this.queueCacheInterval)
8284

83-
for (const worker of this.workers.values()) {
84-
if (!INTERNAL_QUEUES[worker.name]) {
85-
await this.offWork(worker.name)
86-
}
87-
}
85+
await Promise.allSettled(
86+
[...this.workers.values()]
87+
.filter(worker => !INTERNAL_QUEUES[worker.name])
88+
.map(async worker => await this.offWork(worker.name, { wait: false }))
89+
)
8890
}
8991

9092
async failWip () {
@@ -132,11 +134,15 @@ class Manager extends EventEmitter implements types.EventsMixin {
132134

133135
const data = this.getWorkers()
134136
.map(i => i.toWipData())
135-
.filter(i => i.count > 0 && (!INTERNAL_QUEUES[i.name] || includeInternal))
137+
.filter(i => i.state !== 'stopped' && (!INTERNAL_QUEUES[i.name] || includeInternal))
136138

137139
return data
138140
}
139141

142+
hasPendingCleanups (): boolean {
143+
return this.pendingOffWorkCleanups.size > 0
144+
}
145+
140146
private async watch<T> (name: string, options: types.ResolvedWorkOptions, callback: types.WorkHandler<T>): Promise<string> {
141147
if (this.stopped) {
142148
throw new Error('Workers are disabled. pg-boss is stopped')
@@ -190,18 +196,13 @@ class Manager extends EventEmitter implements types.EventsMixin {
190196
return id
191197
}
192198

193-
async offWork (value: string | types.OffWorkOptions): Promise<void> {
194-
assert(value, 'Missing required argument')
199+
async offWork (name: string, options: types.OffWorkOptions = { wait: true }): Promise<void> {
200+
assert(name, 'queue name is required')
201+
assert(typeof name === 'string', 'queue name must be a string')
195202

196-
const query = (typeof value === 'string')
197-
? { filter: (i: Worker<any>) => i.name === value }
198-
: (typeof value === 'object' && value.id)
199-
? { filter: (i: Worker<any>) => i.id === value.id }
200-
: null
203+
const query = (i: Worker<any>) => options?.id ? i.id === options.id : i.name === name
201204

202-
assert(query, 'Invalid argument. Expected string or object: { id }')
203-
204-
const workers = this.getWorkers().filter(i => query.filter(i) && !i.stopping && !i.stopped)
205+
const workers = this.getWorkers().filter(i => query(i) && !i.stopping && !i.stopped)
205206

206207
if (workers.length === 0) {
207208
return
@@ -211,15 +212,22 @@ class Manager extends EventEmitter implements types.EventsMixin {
211212
worker.stop()
212213
}
213214

214-
setImmediate(async () => {
215+
const cleanupPromise = (async () => {
215216
while (!workers.every(w => w.stopped)) {
216217
await delay(1000)
217218
}
218219

219220
for (const worker of workers) {
220221
this.removeWorker(worker)
221222
}
222-
})
223+
})()
224+
225+
if (options.wait) {
226+
await cleanupPromise
227+
} else {
228+
this.pendingOffWorkCleanups.add(cleanupPromise)
229+
cleanupPromise.finally(() => this.pendingOffWorkCleanups.delete(cleanupPromise))
230+
}
223231
}
224232

225233
notifyWorker (workerId: string): void {

src/timekeeper.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ class Timekeeper extends EventEmitter implements types.EventsMixin {
6969

7070
this.stopped = true
7171

72-
await this.manager.offWork(QUEUES.SEND_IT)
72+
await this.manager.offWork(QUEUES.SEND_IT, { wait: true })
7373

7474
if (this.skewMonitorInterval) {
7575
clearInterval(this.skewMonitorInterval)

src/types.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export interface DatabaseOptions {
3030
connectionString?: string;
3131
max?: number;
3232
db?: IDatabase;
33-
33+
connectionTimeoutMillis?: number;
3434
/** @internal */
3535
debug?: boolean;
3636
}
@@ -246,11 +246,11 @@ export interface StopOptions {
246246
close?: boolean;
247247
graceful?: boolean;
248248
timeout?: number;
249-
wait?: boolean;
250249
}
251250

252251
export interface OffWorkOptions {
253-
id: string
252+
id?: string,
253+
wait?: boolean
254254
}
255255

256256
export interface EventsMixin extends NodeJS.EventEmitter {

0 commit comments

Comments
 (0)