pg-boss 12.35.0 → 12.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/attorney.d.ts.map +1 -1
- package/dist/attorney.js +21 -0
- package/dist/boss.d.ts.map +1 -1
- package/dist/boss.js +35 -4
- package/dist/db.d.ts +1 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +11 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -5
- package/dist/latency.d.ts +15 -0
- package/dist/latency.d.ts.map +1 -0
- package/dist/latency.js +45 -0
- package/dist/manager.d.ts +2 -1
- package/dist/manager.d.ts.map +1 -1
- package/dist/manager.js +216 -73
- package/dist/migrationStore.d.ts.map +1 -1
- package/dist/migrationStore.js +67 -0
- package/dist/nurse.d.ts +20 -0
- package/dist/nurse.d.ts.map +1 -0
- package/dist/nurse.js +287 -0
- package/dist/plans.d.ts +20 -0
- package/dist/plans.d.ts.map +1 -1
- package/dist/plans.js +500 -50
- package/dist/registrar.d.ts +16 -0
- package/dist/registrar.d.ts.map +1 -0
- package/dist/registrar.js +221 -0
- package/dist/schema.json +480 -1
- package/dist/telemetry.d.ts +65 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +349 -0
- package/dist/types.d.ts +201 -3
- package/dist/types.d.ts.map +1 -1
- package/package.json +17 -8
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import EventEmitter from 'node:events';
|
|
2
|
+
import type Manager from './manager.ts';
|
|
3
|
+
import * as types from './types.ts';
|
|
4
|
+
declare class Registrar extends EventEmitter implements types.EventsMixin {
|
|
5
|
+
#private;
|
|
6
|
+
readonly id: string;
|
|
7
|
+
events: {
|
|
8
|
+
error: string;
|
|
9
|
+
};
|
|
10
|
+
constructor(id: string, db: types.IDatabase, manager: Manager, config: types.ResolvedConstructorOptions);
|
|
11
|
+
start(): Promise<void>;
|
|
12
|
+
stop(): Promise<void>;
|
|
13
|
+
getInstances(): Promise<types.Instance[]>;
|
|
14
|
+
}
|
|
15
|
+
export default Registrar;
|
|
16
|
+
//# sourceMappingURL=registrar.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registrar.d.ts","sourceRoot":"","sources":["../src/registrar.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,MAAM,aAAa,CAAA;AAItC,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA;AAEvC,OAAO,KAAK,KAAK,MAAM,YAAY,CAAA;AAiCnC,cAAM,SAAU,SAAQ,YAAa,YAAW,KAAK,CAAC,WAAW;;IAC/D,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IAWnB,MAAM;;MAAS;gBAEF,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,KAAK,CAAC,SAAS,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,0BAA0B;IASlG,KAAK;IAuBL,IAAI;IAuBJ,YAAY,IAAK,OAAO,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;CA4IjD;AAED,eAAe,SAAS,CAAA"}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import EventEmitter from 'node:events';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import packageJson from '../package.json' with { type: 'json' };
|
|
4
|
+
import Nurse from "./nurse.js";
|
|
5
|
+
import * as plans from "./plans.js";
|
|
6
|
+
import * as types from "./types.js";
|
|
7
|
+
const events = {
|
|
8
|
+
error: 'error'
|
|
9
|
+
};
|
|
10
|
+
// The constructor options recorded in the registry, so instances sharing a database can be compared.
|
|
11
|
+
// An allowlist on purpose: the table is readable by anything that can read the schema, and an option
|
|
12
|
+
// added later that carries a credential, a connection string or an object must not land in it by
|
|
13
|
+
// default. The adapter itself is recorded only as whether one was given.
|
|
14
|
+
const RECORDED_OPTIONS = [
|
|
15
|
+
'backend', 'max', 'useListenNotify', 'instanceHeartbeatSeconds',
|
|
16
|
+
'supervise', 'schedule', 'migrate', 'createSchema',
|
|
17
|
+
'superviseIntervalSeconds', 'maintenanceIntervalSeconds', 'monitorIntervalSeconds', 'queueCacheIntervalSeconds',
|
|
18
|
+
'bamIntervalSeconds', 'flowIntervalSeconds', 'reindex', 'reindexIntervalSeconds', 'monitorVacuum',
|
|
19
|
+
'clockMonitorIntervalSeconds', 'cronMonitorIntervalSeconds', 'cronWorkerIntervalSeconds',
|
|
20
|
+
'persistWarnings', 'warningRetentionDays', 'persistQueueStats', 'queueStatRetentionDays',
|
|
21
|
+
'warningSlowQuerySeconds', 'warningQueueSize'
|
|
22
|
+
];
|
|
23
|
+
// The work() options recorded on each worker entry, besides the three it keeps as fields. The same
|
|
24
|
+
// allowlist reasoning: groupConcurrency tiers are names, never data, and nothing here connects.
|
|
25
|
+
const RECORDED_WORK_OPTIONS = [
|
|
26
|
+
'includeMetadata', 'ignoreStartAfter', 'minPriority', 'maxPriority', 'localGroupConcurrency', 'groupConcurrency',
|
|
27
|
+
'heartbeatRefreshSeconds', 'perJobResults', 'transactional', 'transactionTimeoutSeconds', 'notifyPollingIntervalSeconds',
|
|
28
|
+
'burstWhenReadyExceeds', 'burstWhenBatchFull'
|
|
29
|
+
];
|
|
30
|
+
// Keeps this PgBoss object's row in the instance table: registered at start(), refreshed on its own
|
|
31
|
+
// heartbeat timer whether or not this instance supervises (an instance with supervise off is exactly
|
|
32
|
+
// the kind people lose track of), and marked stopped by a graceful stop(). Nothing on the job path
|
|
33
|
+
// waits for it. A failed write is emitted as an error and the next beat tries again, and since a beat
|
|
34
|
+
// is an upsert, it also restores a row a failed registration or the retention prune left missing.
|
|
35
|
+
class Registrar extends EventEmitter {
|
|
36
|
+
id;
|
|
37
|
+
#db;
|
|
38
|
+
#manager;
|
|
39
|
+
#config;
|
|
40
|
+
#timer;
|
|
41
|
+
#beating = null;
|
|
42
|
+
#startedOn = null;
|
|
43
|
+
#nurse = new Nurse();
|
|
44
|
+
#recount;
|
|
45
|
+
#active = false;
|
|
46
|
+
events = events;
|
|
47
|
+
constructor(id, db, manager, config) {
|
|
48
|
+
super();
|
|
49
|
+
this.id = id;
|
|
50
|
+
this.#db = db;
|
|
51
|
+
this.#manager = manager;
|
|
52
|
+
this.#config = config;
|
|
53
|
+
}
|
|
54
|
+
async start() {
|
|
55
|
+
if (!this.#config.registerInstance || this.#timer)
|
|
56
|
+
return;
|
|
57
|
+
this.#active = true;
|
|
58
|
+
this.#nurse.start();
|
|
59
|
+
try {
|
|
60
|
+
const { rows } = await this.#db.executeSql(plans.registerInstance(this.#config.schema), await this.#values());
|
|
61
|
+
this.#startedOn = rows[0]?.startedOn ?? null;
|
|
62
|
+
// Counted before the pruning, which keeps the earliest rows' counts only if they are already right.
|
|
63
|
+
await this.#countCrashRestarts();
|
|
64
|
+
// A crash-looping process registers once per life, so this is where its dead rows are bounded.
|
|
65
|
+
await this.#db.executeSql(plans.pruneInstanceLives(this.#config.schema, plans.INSTANCE_DEAD_KEPT_PER_NAME), [this.id, this.#config.instanceName ?? null, os.hostname()]);
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
this.emit(events.error, err);
|
|
69
|
+
}
|
|
70
|
+
this.#timer = this.#config.clock.setInterval(() => { this.#beat(); }, this.#config.instanceHeartbeatSeconds * 1000);
|
|
71
|
+
}
|
|
72
|
+
async stop() {
|
|
73
|
+
if (!this.#timer)
|
|
74
|
+
return;
|
|
75
|
+
this.#active = false;
|
|
76
|
+
this.#config.clock.clearInterval(this.#timer);
|
|
77
|
+
this.#timer = undefined;
|
|
78
|
+
if (this.#recount !== undefined) {
|
|
79
|
+
this.#config.clock.clearTimeout(this.#recount);
|
|
80
|
+
this.#recount = undefined;
|
|
81
|
+
}
|
|
82
|
+
await this.#beating;
|
|
83
|
+
this.#nurse.stop();
|
|
84
|
+
// Best effort and silent: a stop that cannot reach the database leaves a row that goes quiet
|
|
85
|
+
// after three missed heartbeats, which is the same ending a crash gets, and a host that closed
|
|
86
|
+
// its own adapter before stopping pg-boss should not hear about it.
|
|
87
|
+
try {
|
|
88
|
+
await this.#db.executeSql(plans.stopInstance(this.#config.schema), [this.id]);
|
|
89
|
+
}
|
|
90
|
+
catch { }
|
|
91
|
+
}
|
|
92
|
+
async getInstances() {
|
|
93
|
+
const { rows } = await this.#db.executeSql(plans.getInstances(this.#config.schema));
|
|
94
|
+
// CockroachDB returns its INT8 columns as strings.
|
|
95
|
+
const num = (v) => (v === null || v === undefined ? null : Number(v));
|
|
96
|
+
return rows.map(row => ({
|
|
97
|
+
...row,
|
|
98
|
+
pid: Number(row.pid),
|
|
99
|
+
heartbeatSeconds: Number(row.heartbeatSeconds),
|
|
100
|
+
poolMax: num(row.poolMax),
|
|
101
|
+
poolTotal: num(row.poolTotal),
|
|
102
|
+
poolIdle: num(row.poolIdle),
|
|
103
|
+
poolWaiting: num(row.poolWaiting),
|
|
104
|
+
workers: typeof row.workers === 'string' ? JSON.parse(row.workers) : row.workers,
|
|
105
|
+
metrics: typeof row.metrics === 'string' ? JSON.parse(row.metrics) : row.metrics,
|
|
106
|
+
config: typeof row.config === 'string' ? JSON.parse(row.config) : row.config,
|
|
107
|
+
crashRestarts: Number(row.crashRestarts)
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
// Counts this instance's crash restarts, and when a row on its name and host still reads live
|
|
111
|
+
// without having beaten since this instance started, counts again once that row would go quiet.
|
|
112
|
+
async #countCrashRestarts() {
|
|
113
|
+
const clock = this.#config.clock;
|
|
114
|
+
const processStart = new Date(clock.now() - process.uptime() * 1000);
|
|
115
|
+
const values = [this.id, this.#config.instanceName ?? null, os.hostname(), process.pid, processStart];
|
|
116
|
+
await this.#db.executeSql(plans.countCrashRestarts(this.#config.schema), values);
|
|
117
|
+
const { rows } = await this.#db.executeSql(plans.crashRecountAt(this.#config.schema), values);
|
|
118
|
+
const at = rows[0]?.recountAt ? new Date(rows[0].recountAt).getTime() : null;
|
|
119
|
+
if (at === null || !this.#active)
|
|
120
|
+
return;
|
|
121
|
+
this.#recount = clock.setTimeout(() => {
|
|
122
|
+
this.#recount = undefined;
|
|
123
|
+
this.#countCrashRestarts().catch(err => this.emit(events.error, err));
|
|
124
|
+
}, Math.max(1000, at - clock.now() + 1000));
|
|
125
|
+
}
|
|
126
|
+
#beat() {
|
|
127
|
+
// One beat at a time: a slow one is not stacked on, the next tick simply finds it running.
|
|
128
|
+
if (this.#beating)
|
|
129
|
+
return;
|
|
130
|
+
this.#beating = (async () => {
|
|
131
|
+
try {
|
|
132
|
+
await this.#db.executeSql(plans.heartbeatInstance(this.#config.schema), [...await this.#values(), this.#startedOn]);
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
this.emit(events.error, err);
|
|
136
|
+
}
|
|
137
|
+
finally {
|
|
138
|
+
this.#beating = null;
|
|
139
|
+
}
|
|
140
|
+
})();
|
|
141
|
+
}
|
|
142
|
+
// In the column order of plans.registerInstance.
|
|
143
|
+
async #values() {
|
|
144
|
+
const config = this.#config;
|
|
145
|
+
const pool = typeof this.#db.poolCounts === 'function' ? this.#db.poolCounts() : null;
|
|
146
|
+
return [
|
|
147
|
+
this.id,
|
|
148
|
+
config.instanceName ?? null,
|
|
149
|
+
os.hostname(),
|
|
150
|
+
process.pid,
|
|
151
|
+
packageJson.version,
|
|
152
|
+
process.version,
|
|
153
|
+
config.instanceHeartbeatSeconds,
|
|
154
|
+
!!config.supervise,
|
|
155
|
+
!!config.schedule,
|
|
156
|
+
!!config.migrate,
|
|
157
|
+
!!config.persistQueueStats,
|
|
158
|
+
!!config.persistWarnings,
|
|
159
|
+
pool?.max ?? null,
|
|
160
|
+
pool?.total ?? null,
|
|
161
|
+
pool?.idle ?? null,
|
|
162
|
+
pool?.waiting ?? null,
|
|
163
|
+
JSON.stringify(this.#workers()),
|
|
164
|
+
JSON.stringify(await this.#nurse.check()),
|
|
165
|
+
JSON.stringify(this.#recordedConfig())
|
|
166
|
+
];
|
|
167
|
+
}
|
|
168
|
+
// Options left unset are absent rather than null, so two instances on the same defaults compare equal.
|
|
169
|
+
#recordedConfig() {
|
|
170
|
+
const config = { adapter: this.#config.db ? 'custom' : 'pg' };
|
|
171
|
+
for (const key of RECORDED_OPTIONS) {
|
|
172
|
+
const value = this.#config[key];
|
|
173
|
+
if (value !== undefined)
|
|
174
|
+
config[key] = value;
|
|
175
|
+
}
|
|
176
|
+
return config;
|
|
177
|
+
}
|
|
178
|
+
// One entry per work() call. Each call spawns localConcurrency workers sharing a workId, so they are
|
|
179
|
+
// folded back together here: jobs in hand summed, the latest times kept. Only lastErrorOn is
|
|
180
|
+
// stored, never the error, since its message can carry job data and this table is readable by
|
|
181
|
+
// anything that can read the schema.
|
|
182
|
+
#workers() {
|
|
183
|
+
const byWork = new Map();
|
|
184
|
+
const latest = (a, b) => {
|
|
185
|
+
if (b === null)
|
|
186
|
+
return a;
|
|
187
|
+
const iso = new Date(b).toISOString();
|
|
188
|
+
return a === null || iso > a ? iso : a;
|
|
189
|
+
};
|
|
190
|
+
for (const w of this.#manager.getWipData()) {
|
|
191
|
+
let entry = byWork.get(w.workId);
|
|
192
|
+
if (!entry) {
|
|
193
|
+
entry = {
|
|
194
|
+
id: w.workId,
|
|
195
|
+
queue: w.name,
|
|
196
|
+
localConcurrency: 0,
|
|
197
|
+
batchSize: w.options.batchSize ?? 1,
|
|
198
|
+
pollingIntervalSeconds: w.options.pollingIntervalSeconds ?? null,
|
|
199
|
+
active: 0,
|
|
200
|
+
lastFetchedOn: null,
|
|
201
|
+
lastJobEndedOn: null,
|
|
202
|
+
lastErrorOn: null,
|
|
203
|
+
options: {}
|
|
204
|
+
};
|
|
205
|
+
for (const key of RECORDED_WORK_OPTIONS) {
|
|
206
|
+
const value = w.options[key];
|
|
207
|
+
if (value !== undefined)
|
|
208
|
+
entry.options[key] = value;
|
|
209
|
+
}
|
|
210
|
+
byWork.set(w.workId, entry);
|
|
211
|
+
}
|
|
212
|
+
entry.localConcurrency++;
|
|
213
|
+
entry.active += w.count;
|
|
214
|
+
entry.lastFetchedOn = latest(entry.lastFetchedOn, w.lastFetchedOn);
|
|
215
|
+
entry.lastJobEndedOn = latest(entry.lastJobEndedOn, w.lastJobEndedOn);
|
|
216
|
+
entry.lastErrorOn = latest(entry.lastErrorOn, w.lastErrorOn);
|
|
217
|
+
}
|
|
218
|
+
return [...byWork.values()];
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
export default Registrar;
|