pg-boss 12.29.0 → 12.30.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/dist/attorney.d.ts.map +1 -1
- package/dist/attorney.js +12 -3
- package/dist/boss.d.ts.map +1 -1
- package/dist/boss.js +421 -14
- package/dist/manager.d.ts.map +1 -1
- package/dist/manager.js +47 -1
- package/dist/migrationStore.d.ts.map +1 -1
- package/dist/migrationStore.js +150 -27
- package/dist/plans.d.ts +61 -3
- package/dist/plans.d.ts.map +1 -1
- package/dist/plans.js +466 -66
- package/dist/schema.json +42 -14
- package/dist/types.d.ts +27 -7
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
package/dist/attorney.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attorney.d.ts","sourceRoot":"","sources":["../src/attorney.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,KAAK,MAAM,YAAY,CAAA;AAExC,QAAA,MAAM,MAAM;;;;CAIX,CAAA;
|
|
1
|
+
{"version":3,"file":"attorney.d.ts","sourceRoot":"","sources":["../src/attorney.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,KAAK,MAAM,YAAY,CAAA;AAExC,QAAA,MAAM,MAAM;;;;CAIX,CAAA;AAkFD,iBAAS,iBAAiB,CAAE,MAAM,GAAE,GAAQ,QAc3C;AAgBD,iBAAS,mBAAmB,CAAE,KAAK,EAAE,MAAM,UAE1C;AAED,iBAAS,aAAa,CAAE,IAAI,EAAE,GAAG,GAAG,KAAK,CAAC,OAAO,CAgDhD;AAOD,iBAAS,eAAe,CAAE,IAAI,EAAE,GAAG,EAAE,EAAE,MAAc,EAAE;;CAAK,GAAG,KAAK,CAAC,OAAO,CA4D3E;AAED,iBAAS,mBAAmB,CAAE,MAAM,EAAE,GAAG,QAOxC;AAED,iBAAS,gBAAgB,CAAE,IAAI,EAAE,KAAK,CAAC,OAAO,EAAE,QA2D/C;AA2GD,iBAAS,aAAa,CAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG;IAClD,OAAO,EAAE,KAAK,CAAC,mBAAmB,CAAA;IAClC,QAAQ,EAAE,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,CAAA;CACjC,CA8BA;AAED,iBAAS,cAAc,CAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,QAQlD;AAED,iBAAS,SAAS,CAAE,KAAK,EAAE,MAAM,GAAG,KAAK,CAAC,kBAAkB,GAAG,KAAK,CAAC,0BAA0B,CAyB9F;AA4FD,iBAAS,wBAAwB,CAAE,IAAI,EAAE,MAAM,QAiC9C;AAED,iBAAS,eAAe,CAAE,IAAI,EAAE,MAAM,QAIrC;AAED,iBAAS,SAAS,CAAE,GAAG,EAAE,MAAM,QAI9B;AA+LD,OAAO,EACL,SAAS,EACT,wBAAwB,EACxB,eAAe,EACf,cAAc,EACd,aAAa,EACb,eAAe,EACf,aAAa,EACb,SAAS,EACT,mBAAmB,EACnB,MAAM,EACN,gBAAgB,EAChB,mBAAmB,EACnB,iBAAiB,EAClB,CAAA"}
|
package/dist/attorney.js
CHANGED
|
@@ -17,7 +17,8 @@ const COMPATIBILITY_FLAGS = [
|
|
|
17
17
|
'noCoveringIndexes',
|
|
18
18
|
'noListenNotify',
|
|
19
19
|
'noIndexProgressView',
|
|
20
|
-
'noReindex'
|
|
20
|
+
'noReindex',
|
|
21
|
+
'noMonitorVacuum'
|
|
21
22
|
];
|
|
22
23
|
// The single source of truth for backend presets, mirrored by test/testHelper.ts.
|
|
23
24
|
const BACKEND_PROFILES = {
|
|
@@ -39,7 +40,8 @@ const BACKEND_PROFILES = {
|
|
|
39
40
|
// CONCURRENTLY, "unimplemented: this syntax" without — and the bloat check itself cannot run:
|
|
40
41
|
// there is no pg_relation_size(), and reltuples / relpages is an "unsupported binary
|
|
41
42
|
// operator: <float4> / <int4>".
|
|
42
|
-
noReindex: true
|
|
43
|
+
noReindex: true,
|
|
44
|
+
noMonitorVacuum: true
|
|
43
45
|
}
|
|
44
46
|
},
|
|
45
47
|
yugabytedb: {
|
|
@@ -53,7 +55,8 @@ const BACKEND_PROFILES = {
|
|
|
53
55
|
// "REINDEX not supported yet" with or without CONCURRENTLY, and DocDB compaction owns
|
|
54
56
|
// reclamation. The bloat check runs but can never match: relpages and pg_relation_size() are
|
|
55
57
|
// 0 for every relation.
|
|
56
|
-
noReindex: true
|
|
58
|
+
noReindex: true,
|
|
59
|
+
noMonitorVacuum: true
|
|
57
60
|
}
|
|
58
61
|
},
|
|
59
62
|
// No noIndexProgressView: pg-boss keeps its tables coordinator-local (it never calls
|
|
@@ -570,6 +573,7 @@ function applyOpsConfig(config) {
|
|
|
570
573
|
}
|
|
571
574
|
config.queueStatRetentionDays = config.queueStatRetentionDays || 7;
|
|
572
575
|
validateReindexConfig(config);
|
|
576
|
+
validateVacuumMonitorConfig(config);
|
|
573
577
|
}
|
|
574
578
|
function validateReindexConfig(config) {
|
|
575
579
|
assert(!('reindex' in config) || typeof config.reindex === 'boolean' || (typeof config.reindex === 'object' && config.reindex !== null), 'configuration assert: reindex must be a boolean or an options object');
|
|
@@ -587,6 +591,11 @@ function validateReindexConfig(config) {
|
|
|
587
591
|
config.reindexIntervalSeconds = config.reindexIntervalSeconds || POLICY.MAX_EXPIRATION_HOURS * 60 * 60;
|
|
588
592
|
assert(config.reindexIntervalSeconds / 60 / 60 <= POLICY.MAX_EXPIRATION_HOURS, `configuration assert: reindexIntervalSeconds cannot exceed ${POLICY.MAX_EXPIRATION_HOURS} hours`);
|
|
589
593
|
}
|
|
594
|
+
function validateVacuumMonitorConfig(config) {
|
|
595
|
+
// A switch, not a threshold: the check derives its trigger from the server's own autovacuum
|
|
596
|
+
// settings and from measured dead tuples, so there is no number here for a caller to get wrong.
|
|
597
|
+
assert(!('monitorVacuum' in config) || typeof config.monitorVacuum === 'boolean', 'configuration assert: monitorVacuum must be a boolean');
|
|
598
|
+
}
|
|
590
599
|
function validateDeletionConfig(config) {
|
|
591
600
|
assert(!('deleteAfterSeconds' in config) || config.deleteAfterSeconds >= 0, 'configuration assert: deleteAfterSeconds must be at least 0 (0 disables deletion)');
|
|
592
601
|
}
|
package/dist/boss.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"boss.d.ts","sourceRoot":"","sources":["../src/boss.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,MAAM,aAAa,CAAA;AACtC,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA;AAGvC,OAAO,KAAK,KAAK,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"boss.d.ts","sourceRoot":"","sources":["../src/boss.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,MAAM,aAAa,CAAA;AACtC,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA;AAGvC,OAAO,KAAK,KAAK,MAAM,YAAY,CAAA;AAgHnC,cAAM,IAAK,SAAQ,YAAa,YAAW,KAAK,CAAC,WAAW;;IAsC1D,MAAM;;;MAAS;gBAGb,EAAE,EAAE,KAAK,CAAC,SAAS,EACnB,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,KAAK,CAAC,0BAA0B;IAa1C,IAAI,WAAW,IAAK,OAAO,CAE1B;IAEK,KAAK;IAWL,IAAI;IA8FJ,SAAS,CAAE,KAAK,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC,WAAW,EAAE,EAAE,OAAO,CAAC,EAAE,KAAK,CAAC,gBAAgB;IAwqBvF;;;;OAIG;IACG,kBAAkB,CAAE,OAAO,CAAC,EAAE,KAAK,CAAC,cAAc,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;CAwB7E;AAED,eAAe,IAAI,CAAA"}
|
package/dist/boss.js
CHANGED
|
@@ -10,13 +10,62 @@ const events = {
|
|
|
10
10
|
// Default thresholds and warning messages
|
|
11
11
|
const WARNINGS = {
|
|
12
12
|
SLOW_QUERY: { seconds: 30, message: 'Warning: slow query. Your queues and/or database server should be reviewed' },
|
|
13
|
-
LARGE_QUEUE: { size: 10_000, message: 'Warning: large queue backlog. Your queue should be reviewed' }
|
|
13
|
+
LARGE_QUEUE: { size: 10_000, message: 'Warning: large queue backlog. Your queue should be reviewed' },
|
|
14
|
+
// No threshold constant: the trigger is measured, not configured. See #checkVacuum.
|
|
15
|
+
XMIN_HORIZON: { message: 'Warning: the database transaction horizon is pinned, so completed jobs cannot be cleaned up' },
|
|
16
|
+
AUTOVACUUM_DISABLED: { message: 'Warning: autovacuum is disabled, so nothing is reclaiming completed jobs' },
|
|
17
|
+
// Deliberately weaker than XMIN_HORIZON, and the difference is the evidence behind each. That one
|
|
18
|
+
// asserts the horizon is pinned and that jobs cannot be reclaimed, having watched a vacuum run and
|
|
19
|
+
// reclaim nothing across two passes. This one has a stopwatch and nothing else. The aggregate does
|
|
20
|
+
// pin the horizon while it runs - that much is definitional - but whether any reclamation was
|
|
21
|
+
// actually lost to it is not measured here, so this reports the exposure and the action taken and
|
|
22
|
+
// leaves the diagnosis to XMIN_HORIZON, which will fire on its own if the harm is real.
|
|
23
|
+
MONITOR_BACKOFF: { message: 'Warning: queue monitoring spent long enough scanning the job table to risk holding back autovacuum, so the next stats refresh is deferred' }
|
|
14
24
|
};
|
|
15
25
|
const WARNING_TYPES = {
|
|
16
26
|
SLOW_QUERY: 'slow_query',
|
|
17
27
|
QUEUE_BACKLOG: 'queue_backlog',
|
|
18
|
-
INDEX_BLOAT: 'index_bloat'
|
|
28
|
+
INDEX_BLOAT: 'index_bloat',
|
|
29
|
+
XMIN_HORIZON: 'xmin_horizon',
|
|
30
|
+
AUTOVACUUM_DISABLED: 'autovacuum_disabled',
|
|
31
|
+
MONITOR_BACKOFF: 'monitor_backoff'
|
|
19
32
|
};
|
|
33
|
+
// Which holder class each source in the horizon query represents, phrased so the warning names
|
|
34
|
+
// something an operator can go and look at.
|
|
35
|
+
const XMIN_HOLDERS = {
|
|
36
|
+
backends: 'a backend holding an open transaction',
|
|
37
|
+
slots: 'a replication slot',
|
|
38
|
+
slotsCatalog: 'a replication slot (catalog xmin)',
|
|
39
|
+
standbys: 'a standby with hot_standby_feedback enabled',
|
|
40
|
+
prepared: 'a prepared transaction'
|
|
41
|
+
};
|
|
42
|
+
// Name the holder as specifically as the catalog allowed. Falls back to the holder class when the
|
|
43
|
+
// source is not a backend (a replication slot has no pid to report) or when no backend row came
|
|
44
|
+
// back, so the warning never loses the description it had before.
|
|
45
|
+
function describeXminHolder(source, row) {
|
|
46
|
+
const holder = source === 'backends' ? row.backendHolder : null;
|
|
47
|
+
if (!holder)
|
|
48
|
+
return XMIN_HOLDERS[source];
|
|
49
|
+
const app = holder.applicationName || null;
|
|
50
|
+
const where = [
|
|
51
|
+
app && `application_name '${app}'`,
|
|
52
|
+
holder.userName && `role ${holder.userName}`,
|
|
53
|
+
`pid ${holder.pid}`
|
|
54
|
+
].filter(Boolean).join(', ');
|
|
55
|
+
const open = holder.xactSeconds === null || holder.xactSeconds === undefined
|
|
56
|
+
? 'for an unreadable length of time'
|
|
57
|
+
: `for ${holder.xactSeconds}s`;
|
|
58
|
+
// Three outcomes, not two, because an empty application_name cannot be classified either way and
|
|
59
|
+
// saying "another application" would be asserting something unmeasured. Comparison is against
|
|
60
|
+
// this connection's own value rather than the literal 'pgboss': an adapter-supplied pool carries
|
|
61
|
+
// whatever the host app set, so "same application_name as us" is the strongest true claim.
|
|
62
|
+
const who = !app
|
|
63
|
+
? 'a backend with no application_name'
|
|
64
|
+
: app === row.selfApplicationName
|
|
65
|
+
? "this application's own connection"
|
|
66
|
+
: 'another application';
|
|
67
|
+
return `${who} (${where}) has held a transaction open ${open}`;
|
|
68
|
+
}
|
|
20
69
|
// SQLSTATE 25001 (active_sql_transaction): "REINDEX CONCURRENTLY cannot run inside a transaction
|
|
21
70
|
// block". Raised when a user-supplied adapter wraps executeSql in a transaction — a property of the
|
|
22
71
|
// adapter, not of this pass, so it disables rebuilds for the life of the instance rather than
|
|
@@ -41,6 +90,24 @@ class Boss extends EventEmitter {
|
|
|
41
90
|
// Local rate limit for passes that only report bloat, which deliberately leave the shared interval
|
|
42
91
|
// claim to whichever instance can act on it.
|
|
43
92
|
#detectOnly = 0;
|
|
93
|
+
// Latched while a job table is over its autovacuum budget, so a sustained pin warns once rather
|
|
94
|
+
// than every pass. Cleared when every table is back under budget, so a later episode warns again.
|
|
95
|
+
#warnedXminHorizon = false;
|
|
96
|
+
// Same latching for the sibling diagnosis: garbage growing on a table nothing vacuums at all.
|
|
97
|
+
#warnedAutovacuumDisabled = false;
|
|
98
|
+
// Per job table: the dead-tuple count and vacuum timestamp last seen while the table was over its
|
|
99
|
+
// autovacuum budget. Two observations are what turn "there is garbage" into "a vacuum ran and
|
|
100
|
+
// reclaimed none of it" - see #checkXminHorizon.
|
|
101
|
+
#garbageEvidence = new Map();
|
|
102
|
+
// Sources the connected role could not read. Narrowed once on first failure rather than retried
|
|
103
|
+
// every pass; an empty-but-unreadable source must never be reported as healthy.
|
|
104
|
+
#xminHorizonSources = null;
|
|
105
|
+
// Seconds this supervise pass held the MVCC horizon inside the queue-stats aggregate, as the
|
|
106
|
+
// server measured it, summed across every table and every 100-name chunk. The sum, not the worst
|
|
107
|
+
// single query: chunking and partitioned queues both multiply the aggregate within one pass (each
|
|
108
|
+
// chunk re-reads the whole heap - measured at 384,632 buffers for 2 of 20 queue names), and it is
|
|
109
|
+
// the total contiguous pin that starves autovacuum, not any one scan.
|
|
110
|
+
#statsElapsedSeconds = 0;
|
|
44
111
|
events = events;
|
|
45
112
|
constructor(db, manager, config) {
|
|
46
113
|
super();
|
|
@@ -176,6 +243,12 @@ class Boss extends EventEmitter {
|
|
|
176
243
|
}
|
|
177
244
|
if (this.#stopping)
|
|
178
245
|
return;
|
|
246
|
+
// Immediately after the aggregates it measures, not at the head of the next pass. Deferring the
|
|
247
|
+
// write left a whole supervise interval between a slow aggregate finishing and the gate closing
|
|
248
|
+
// behind it, and a getQueueStats({ force: true }) landing in that window ran exactly the scan
|
|
249
|
+
// the backoff was about to prevent.
|
|
250
|
+
await this.#applyMonitorBackoff();
|
|
251
|
+
await this.#checkVacuum();
|
|
179
252
|
await this.#maintainWarnings();
|
|
180
253
|
await this.#maintainQueueStats();
|
|
181
254
|
// Last in the pass: a rebuild is DDL that can run for seconds, so nothing time-sensitive
|
|
@@ -191,21 +264,44 @@ class Boss extends EventEmitter {
|
|
|
191
264
|
return;
|
|
192
265
|
if (rows.length) {
|
|
193
266
|
const queues = rows.map((q) => q.name);
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
267
|
+
// The vacuum-safety backoff defers the stats aggregate, not the whole pass. Job expiry and
|
|
268
|
+
// heartbeat failure below are narrow indexed updates that pin nothing, and gating them on the
|
|
269
|
+
// horizon would turn a vacuum-safety valve into a job-expiry outage lasting two naptimes at
|
|
270
|
+
// minimum. See the note on plans.trySetQueueMonitorTime. The flag is a property of the
|
|
271
|
+
// statement, not of a queue, so every row carries the same value.
|
|
272
|
+
const refreshStats = rows[0].refreshStats !== false;
|
|
273
|
+
if (refreshStats) {
|
|
274
|
+
const cacheStatsSql = plans.cacheQueueStats(this.#config.schema, table, queues, this.#config.noAdvisoryLocks);
|
|
275
|
+
// The pin this pass cost, taken from the server's own clock (see the pinSeconds column in
|
|
276
|
+
// cacheQueueStats) and not from a stopwatch around the call - that would count pool wait,
|
|
277
|
+
// network and event-loop lag, none of which hold the horizon. The client measurement stays as
|
|
278
|
+
// a fallback for a backend or adapter that returns no rows to read it from.
|
|
279
|
+
const statsStarted = Date.now();
|
|
280
|
+
const { rows: rowsCacheStats } = await this.#executeQuery(cacheStatsSql);
|
|
281
|
+
const pinned = rowsCacheStats.reduce((max, row) => Math.max(max, Number(row.pinSeconds) || 0), 0);
|
|
282
|
+
// Only when the aggregate actually ran. An empty result means another instance holds the
|
|
283
|
+
// stats try-lock and this statement returned without scanning anything, so the elapsed
|
|
284
|
+
// client time is pool and network latency - exactly the measurement the server-side
|
|
285
|
+
// pinSeconds column exists to avoid backing off on.
|
|
286
|
+
if (rowsCacheStats.length) {
|
|
287
|
+
this.#statsElapsedSeconds += pinned || (Date.now() - statsStarted) / 1000;
|
|
288
|
+
}
|
|
289
|
+
if (this.#config.persistQueueStats) {
|
|
290
|
+
const insertSql = plans.insertQueueStats(this.#config.schema, queues, this.#config.noAdvisoryLocks);
|
|
291
|
+
await this.#executeQuery(insertSql);
|
|
292
|
+
}
|
|
293
|
+
if (this.#stopping)
|
|
294
|
+
return;
|
|
295
|
+
// Coerce with Number(): CockroachDB returns these integer columns as strings, so a bare `>`
|
|
296
|
+
// would compare lexicographically ("100" > "9" === false) and silently miss the backlog. On
|
|
297
|
+
// standard Postgres these are already numbers, so Number() is a no-op.
|
|
298
|
+
const warnings = rowsCacheStats.filter(i => Number(i.queuedCount) > (Number(i.warningQueueSize) || this.#largeQueueSize));
|
|
299
|
+
for (const warning of warnings) {
|
|
300
|
+
await emitAndPersistWarning(this.#warningContext, WARNING_TYPES.QUEUE_BACKLOG, WARNINGS.LARGE_QUEUE.message, warning);
|
|
301
|
+
}
|
|
199
302
|
}
|
|
200
303
|
if (this.#stopping)
|
|
201
304
|
return;
|
|
202
|
-
// Coerce with Number(): CockroachDB returns these integer columns as strings, so a bare `>`
|
|
203
|
-
// would compare lexicographically ("100" > "9" === false) and silently miss the backlog. On
|
|
204
|
-
// standard Postgres these are already numbers, so Number() is a no-op.
|
|
205
|
-
const warnings = rowsCacheStats.filter(i => Number(i.queuedCount) > (Number(i.warningQueueSize) || this.#largeQueueSize));
|
|
206
|
-
for (const warning of warnings) {
|
|
207
|
-
await emitAndPersistWarning(this.#warningContext, WARNING_TYPES.QUEUE_BACKLOG, WARNINGS.LARGE_QUEUE.message, warning);
|
|
208
|
-
}
|
|
209
305
|
// CockroachDB rejects the multi-mutation failJobs() CTE these use, so under noMultiMutationCte
|
|
210
306
|
// route expiry through the manager's split select/delete/re-insert variants instead.
|
|
211
307
|
if (this.#config.noMultiMutationCte) {
|
|
@@ -241,6 +337,317 @@ class Boss extends EventEmitter {
|
|
|
241
337
|
await this.#executeQuery(depSql);
|
|
242
338
|
}
|
|
243
339
|
}
|
|
340
|
+
/**
|
|
341
|
+
* Defer the next queue-stats aggregate when this pass spent long enough inside it to threaten
|
|
342
|
+
* autovacuum, and tell the operator that it happened.
|
|
343
|
+
*
|
|
344
|
+
* The aggregate in cacheQueueStats is a whole-heap scan - the queue-name filter does not reduce
|
|
345
|
+
* the I/O, only the number of rows fed to the aggregate transitions - and for its whole duration
|
|
346
|
+
* it advertises a snapshot Postgres will not vacuum past. Measured on a 3 GB / 10M-row job table:
|
|
347
|
+
* 0.19 s at 1M rows, 2.1 s with parallel workers and 3.6 s without at 10M, and 24-75 s for the
|
|
348
|
+
* same 3 GB read cold from network storage. Against that, a 200,000-row delete stayed
|
|
349
|
+
* "dead but not yet removable" for exactly as long as one such snapshot was held open, and was
|
|
350
|
+
* removed by the next vacuum after it closed.
|
|
351
|
+
*
|
|
352
|
+
* What is measured is the server's own transaction duration, not the latency of the call: a pool
|
|
353
|
+
* with no free connection makes executeSql slow without pinning anything (a 1 ms transaction timed
|
|
354
|
+
* at 1.97 s behind one busy connection), and backing off on that would defer monitoring for pool
|
|
355
|
+
* contention and then tell the operator to go and shrink their job table.
|
|
356
|
+
*
|
|
357
|
+
* So the danger is not the aggregate; it is the aggregate's duty cycle. Three things push it up
|
|
358
|
+
* without anyone changing a default - a slow enough scan that a pass outruns the interval, the
|
|
359
|
+
* 100-name chunking that makes one pass re-read the heap once per chunk, and one aggregate per
|
|
360
|
+
* table on a partitioned deployment - and one thing pushes it up deliberately, which is tuning
|
|
361
|
+
* monitorIntervalSeconds down. All four land in the same place, and #statsElapsedSeconds measures
|
|
362
|
+
* all four the same way.
|
|
363
|
+
*
|
|
364
|
+
* The threshold and the length both come from the server's own autovacuum_naptime rather than
|
|
365
|
+
* from a constant here; see plans.setMonitorBackoff for why the free window has to be longer than
|
|
366
|
+
* a naptime and why two of them is the right number. The statement returns a row only when it
|
|
367
|
+
* engaged, so this warns exactly when monitoring is actually being held back.
|
|
368
|
+
*
|
|
369
|
+
* PostgreSQL-only, and off with the rest of the vacuum subsystem: distributed backends have no
|
|
370
|
+
* autovacuum_naptime to read and reclaim on their own schedule, and an operator who set
|
|
371
|
+
* monitorVacuum: false has taken vacuum health into their own hands.
|
|
372
|
+
*/
|
|
373
|
+
async #applyMonitorBackoff() {
|
|
374
|
+
const elapsed = this.#config.__test__monitor_stats_seconds ?? this.#statsElapsedSeconds;
|
|
375
|
+
this.#statsElapsedSeconds = 0;
|
|
376
|
+
if (!elapsed)
|
|
377
|
+
return;
|
|
378
|
+
if (this.#config.monitorVacuum === false || this.#config.noMonitorVacuum)
|
|
379
|
+
return;
|
|
380
|
+
const { rows } = await this.#executeQuery(plans.setMonitorBackoff(this.#config.schema, elapsed));
|
|
381
|
+
const engaged = rows.at(0);
|
|
382
|
+
if (!engaged)
|
|
383
|
+
return;
|
|
384
|
+
await emitAndPersistWarning(this.#warningContext, WARNING_TYPES.MONITOR_BACKOFF, `${WARNINGS.MONITOR_BACKOFF.message}: ${elapsed.toFixed(1)}s of aggregate against an ` +
|
|
385
|
+
`autovacuum_naptime of ${Number(engaged.naptimeSeconds)}s. ` +
|
|
386
|
+
'Shrink the job table (retention, deleteAfterSeconds) or partition its busiest queues.', {
|
|
387
|
+
elapsedSeconds: elapsed,
|
|
388
|
+
naptimeSeconds: Number(engaged.naptimeSeconds),
|
|
389
|
+
backoffSeconds: Number(engaged.backoffSeconds),
|
|
390
|
+
backoffUntil: engaged.backoffUntil
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Warn when the queues are producing garbage that nothing is reclaiming. Every documented
|
|
395
|
+
* Postgres-queue collapse has this as its precondition, and until now pg-boss reported only the
|
|
396
|
+
* symptom (`queue_backlog`) and never the cause: a backlog caused by too few workers and one
|
|
397
|
+
* caused by unreclaimed garbage look identical from the queue's own counters, and have opposite
|
|
398
|
+
* fixes.
|
|
399
|
+
*
|
|
400
|
+
* One measurement, two diagnoses, because the fixes are completely different:
|
|
401
|
+
*
|
|
402
|
+
* `xmin_horizon` vacuum runs and reclaims nothing - something is pinning the MVCC
|
|
403
|
+
* horizon, and the fix is to find and release the holder.
|
|
404
|
+
* `autovacuum_disabled` nothing runs at all - autovacuum is turned off on the table, and the
|
|
405
|
+
* fix is to turn it back on or vacuum on a schedule that keeps up.
|
|
406
|
+
*
|
|
407
|
+
* The trigger is the harm itself, read from `pg_stat_user_tables`, not a transaction-count proxy.
|
|
408
|
+
* A measured comparison of the two (scripts/xmin-horizon-experiment.js) is what settled this: the
|
|
409
|
+
* transaction count that corresponds to a given amount of unreclaimable garbage moves by ~9x with
|
|
410
|
+
* producer batching alone, and without bound with unrelated traffic on the same cluster, because
|
|
411
|
+
* `age()` is cluster-global. No constant survives that. Dead tuples do not move: the same 40,000
|
|
412
|
+
* jobs left the same 120,000 unreclaimable rows across both producer shapes.
|
|
413
|
+
*
|
|
414
|
+
* Both diagnoses share the first condition - a job table past the point Postgres itself would
|
|
415
|
+
* vacuum it (`autovacuum_vacuum_threshold + scale_factor * live`, per-table storage parameters
|
|
416
|
+
* honoured) - and then split on what two consecutive observations show:
|
|
417
|
+
*
|
|
418
|
+
* a vacuum ran and the dead-tuple count did not fall -> the horizon is pinned
|
|
419
|
+
* no vacuum ran and the dead-tuple count grew -> nothing is vacuuming this table
|
|
420
|
+
*
|
|
421
|
+
* Either way it takes two passes to establish, so the first pass after a problem starts never
|
|
422
|
+
* warns. That is the intended cost of not guessing: one supervise interval of delay buys a signal
|
|
423
|
+
* that means what it says. It is also what keeps an operator who has turned autovacuum off
|
|
424
|
+
* deliberately and vacuums on their own schedule quiet - their manual vacuum both moves the
|
|
425
|
+
* timestamp and drops the count, so neither branch matches.
|
|
426
|
+
*
|
|
427
|
+
* PostgreSQL-only: CockroachDB and YugabyteDB reclaim on their engine's own schedule rather than
|
|
428
|
+
* from the oldest live snapshot, and their profiles set noMonitorVacuum.
|
|
429
|
+
*/
|
|
430
|
+
async #checkVacuum() {
|
|
431
|
+
if (this.#stopping)
|
|
432
|
+
return;
|
|
433
|
+
if (this.#config.monitorVacuum === false || this.#config.noMonitorVacuum)
|
|
434
|
+
return;
|
|
435
|
+
const { rows } = await this.#executeQuery(plans.getJobTableGarbage(this.#config.schema));
|
|
436
|
+
if (this.#stopping)
|
|
437
|
+
return;
|
|
438
|
+
// Vacuum ran and reclaimed nothing, versus nothing ran at all.
|
|
439
|
+
const stuck = [];
|
|
440
|
+
const unvacuumed = [];
|
|
441
|
+
// Tracked separately so one table sitting permanently over budget with autovacuum off cannot
|
|
442
|
+
// pin the other diagnosis's latch and suppress a later, unrelated episode.
|
|
443
|
+
let vacuumedOverBudget = false;
|
|
444
|
+
let disabledOverBudget = false;
|
|
445
|
+
// Partitioned queues come and go, so drop evidence for tables the query no longer returns
|
|
446
|
+
// rather than holding a row per table this instance has ever seen.
|
|
447
|
+
const present = new Set(rows.map(row => row.name));
|
|
448
|
+
for (const name of this.#garbageEvidence.keys()) {
|
|
449
|
+
if (!present.has(name))
|
|
450
|
+
this.#garbageEvidence.delete(name);
|
|
451
|
+
}
|
|
452
|
+
for (const row of rows) {
|
|
453
|
+
const name = row.name;
|
|
454
|
+
const liveTuples = Number(row.liveTuples);
|
|
455
|
+
const deadTuples = Number(row.deadTuples);
|
|
456
|
+
const budget = Number(row.threshold) + Number(row.scaleFactor) * liveTuples;
|
|
457
|
+
const disabled = row.autovacuumEnabled === false;
|
|
458
|
+
// Compared only against the previous observation of the same table, so any monotonic clock
|
|
459
|
+
// works; a table never vacuumed reads as 0 and stays there, which is exactly the state the
|
|
460
|
+
// autovacuum_disabled branch is looking for.
|
|
461
|
+
const lastVacuum = row.lastVacuum ? new Date(row.lastVacuum).getTime() : 0;
|
|
462
|
+
if (deadTuples <= budget) {
|
|
463
|
+
this.#garbageEvidence.delete(name);
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
if (disabled) {
|
|
467
|
+
disabledOverBudget = true;
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
vacuumedOverBudget = true;
|
|
471
|
+
}
|
|
472
|
+
const previous = this.#garbageEvidence.get(name);
|
|
473
|
+
this.#garbageEvidence.set(name, { deadTuples, lastVacuum });
|
|
474
|
+
if (!previous)
|
|
475
|
+
continue;
|
|
476
|
+
const garbage = {
|
|
477
|
+
name,
|
|
478
|
+
liveTuples,
|
|
479
|
+
deadTuples,
|
|
480
|
+
budget,
|
|
481
|
+
// Null rather than 0 when the table has never been vacuumed at all - "never" and "just now"
|
|
482
|
+
// are opposite readings and must not share a value.
|
|
483
|
+
vacuumAgeSeconds: row.vacuumAgeSeconds === null ? null : Number(row.vacuumAgeSeconds),
|
|
484
|
+
lastVacuum
|
|
485
|
+
};
|
|
486
|
+
if (disabled) {
|
|
487
|
+
// Nothing ran between the two observations and the pile grew.
|
|
488
|
+
if (lastVacuum === previous.lastVacuum && deadTuples > previous.deadTuples) {
|
|
489
|
+
unvacuumed.push(garbage);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
else if (lastVacuum > previous.lastVacuum && deadTuples >= previous.deadTuples) {
|
|
493
|
+
// A vacuum ran between the two observations and left at least as much garbage as it found.
|
|
494
|
+
stuck.push(garbage);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (!vacuumedOverBudget)
|
|
498
|
+
this.#warnedXminHorizon = false;
|
|
499
|
+
if (!disabledOverBudget)
|
|
500
|
+
this.#warnedAutovacuumDisabled = false;
|
|
501
|
+
await this.#warnAutovacuumDisabled(unvacuumed);
|
|
502
|
+
await this.#warnXminHorizon(stuck);
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Garbage piling up on a table nothing vacuums. No horizon lookup: there is no vacuum here that
|
|
506
|
+
* could have failed, so there is no holder to blame, and naming one would send the operator after
|
|
507
|
+
* the wrong thing entirely. The fix is autovacuum, not a stuck transaction.
|
|
508
|
+
*/
|
|
509
|
+
async #warnAutovacuumDisabled(unvacuumed) {
|
|
510
|
+
if (!unvacuumed.length || this.#warnedAutovacuumDisabled || this.#stopping)
|
|
511
|
+
return;
|
|
512
|
+
this.#warnedAutovacuumDisabled = true;
|
|
513
|
+
const worst = unvacuumed.reduce((a, b) => (b.deadTuples > a.deadTuples ? b : a));
|
|
514
|
+
const since = worst.vacuumAgeSeconds === null
|
|
515
|
+
? 'has never been vacuumed'
|
|
516
|
+
: `was last vacuumed ${worst.vacuumAgeSeconds}s ago`;
|
|
517
|
+
await emitAndPersistWarning(this.#warningContext, WARNING_TYPES.AUTOVACUUM_DISABLED, `${WARNINGS.AUTOVACUUM_DISABLED.message}: ${worst.name} has autovacuum_enabled = false, holds ` +
|
|
518
|
+
`${worst.deadTuples} dead rows and is still growing, and ${since} ` +
|
|
519
|
+
`(Postgres would vacuum this table at ${Math.round(worst.budget)})`, {
|
|
520
|
+
table: worst.name,
|
|
521
|
+
tables: unvacuumed.map(t => t.name),
|
|
522
|
+
liveTuples: worst.liveTuples,
|
|
523
|
+
deadTuples: worst.deadTuples,
|
|
524
|
+
budget: Math.round(worst.budget),
|
|
525
|
+
vacuumAgeSeconds: worst.vacuumAgeSeconds
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
/** Garbage that survived a vacuum, attributed to whatever is holding the horizon back. */
|
|
529
|
+
async #warnXminHorizon(stuck) {
|
|
530
|
+
if (!stuck.length || this.#warnedXminHorizon || this.#stopping)
|
|
531
|
+
return;
|
|
532
|
+
// The vacuum a holder has to predate is the most recent one that failed, so the newest wins.
|
|
533
|
+
const lastVacuum = new Date(Math.max(...stuck.map(t => t.lastVacuum)));
|
|
534
|
+
const horizon = await this.#readXminHorizon(lastVacuum);
|
|
535
|
+
if (!horizon || this.#stopping)
|
|
536
|
+
return;
|
|
537
|
+
const holder = this.#attributeXminHorizon(horizon.row);
|
|
538
|
+
// Garbage that survived a vacuum with no holder old enough to explain it is a real problem, but
|
|
539
|
+
// not this one - staying quiet is what keeps the warning worth acting on.
|
|
540
|
+
if (!holder)
|
|
541
|
+
return;
|
|
542
|
+
this.#warnedXminHorizon = true;
|
|
543
|
+
const worst = stuck.reduce((a, b) => (b.deadTuples > a.deadTuples ? b : a));
|
|
544
|
+
const seconds = Number(horizon.row.oldestTransactionSeconds);
|
|
545
|
+
const backend = holder.source === 'backends' ? horizon.row.backendHolder ?? null : null;
|
|
546
|
+
const opaque = Number(horizon.row.opaqueBackends) || 0;
|
|
547
|
+
// Only worth saying when the holder could not be named: with a name in hand the operator has
|
|
548
|
+
// what they need, and the grant is beside the point.
|
|
549
|
+
const opaqueNote = opaque && !backend
|
|
550
|
+
? ` ${opaque} backend(s) could not be inspected by this role; GRANT pg_read_all_stats to name them.`
|
|
551
|
+
: '';
|
|
552
|
+
await emitAndPersistWarning(this.#warningContext, WARNING_TYPES.XMIN_HORIZON, `${WARNINGS.XMIN_HORIZON.message}: ${describeXminHolder(holder.source, horizon.row)}, ` +
|
|
553
|
+
`holding it ${holder.age} transactions back. ` +
|
|
554
|
+
`${worst.name} has ${worst.deadTuples} dead rows that a vacuum ${worst.vacuumAgeSeconds}s ago could not reclaim ` +
|
|
555
|
+
`(Postgres vacuums this table at ${Math.round(worst.budget)}).${opaqueNote}`, {
|
|
556
|
+
source: holder.source,
|
|
557
|
+
holder: describeXminHolder(holder.source, horizon.row),
|
|
558
|
+
holderClass: XMIN_HOLDERS[holder.source],
|
|
559
|
+
// Null unless the holder is a backend this role could read a row for. `self` says whether it
|
|
560
|
+
// shares this connection's application_name — pg-boss pinning its own horizon and an
|
|
561
|
+
// external reporting tool pinning it have opposite fixes.
|
|
562
|
+
holderPid: backend?.pid ?? null,
|
|
563
|
+
holderApplicationName: backend?.applicationName ?? null,
|
|
564
|
+
holderUserName: backend?.userName ?? null,
|
|
565
|
+
holderState: backend?.state ?? null,
|
|
566
|
+
holderTransactionSeconds: backend?.xactSeconds ?? null,
|
|
567
|
+
self: backend ? backend.applicationName === horizon.row.selfApplicationName : null,
|
|
568
|
+
// Backends whose transaction this role is not allowed to time. Non-zero means the picture is
|
|
569
|
+
// partial, whether or not a holder was named.
|
|
570
|
+
opaqueBackends: opaque,
|
|
571
|
+
transactions: holder.age,
|
|
572
|
+
table: worst.name,
|
|
573
|
+
liveTuples: worst.liveTuples,
|
|
574
|
+
deadTuples: worst.deadTuples,
|
|
575
|
+
budget: Math.round(worst.budget),
|
|
576
|
+
vacuumAgeSeconds: worst.vacuumAgeSeconds,
|
|
577
|
+
tables: stuck.map(t => t.name),
|
|
578
|
+
oldestTransactionSeconds: Number.isFinite(seconds) ? seconds : null,
|
|
579
|
+
// Named so a partial answer is never mistaken for a clean one.
|
|
580
|
+
unreadableSources: horizon.unreadable
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* The widest holder in the row. Every source the query returns has already been qualified — the
|
|
585
|
+
* backends column is filtered server-side to transactions that predate the failed vacuum, and a
|
|
586
|
+
* slot, standby or prepared transaction advertises an xmin only while something is genuinely
|
|
587
|
+
* stuck — so this is a straight maximum. The horizon is pinned to the oldest of them, which makes
|
|
588
|
+
* the widest the one worth naming.
|
|
589
|
+
*/
|
|
590
|
+
#attributeXminHorizon(row) {
|
|
591
|
+
let worst = null;
|
|
592
|
+
// Iterates the known holder classes, not the row's columns: the row also carries
|
|
593
|
+
// oldestTransactionSeconds, which is informational and would otherwise read as a holder named
|
|
594
|
+
// after a column with an age of 0.
|
|
595
|
+
for (const source of Object.keys(XMIN_HOLDERS)) {
|
|
596
|
+
const value = row[source];
|
|
597
|
+
const age = value === undefined || value === null ? NaN : Number(value);
|
|
598
|
+
if (Number.isFinite(age) && (!worst || age > worst.age)) {
|
|
599
|
+
worst = { source, age };
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return worst;
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Read the horizon, narrowing to the sources this role can actually read. Called only when a
|
|
606
|
+
* table already shows unreclaimable garbage, so an installation with a healthy horizon never
|
|
607
|
+
* pays for it.
|
|
608
|
+
*/
|
|
609
|
+
async #readXminHorizon(lastVacuum) {
|
|
610
|
+
const attempted = this.#xminHorizonSources ?? plans.XMIN_HORIZON_QUERY_SOURCES;
|
|
611
|
+
// Narrowing has already found that this role can read none of them, and the check said so once.
|
|
612
|
+
// Without this the empty list would be handed back to getXminHorizon, which cannot build a
|
|
613
|
+
// statement with no columns, and every later pass would re-fail, re-probe and re-warn.
|
|
614
|
+
if (!attempted.length)
|
|
615
|
+
return null;
|
|
616
|
+
try {
|
|
617
|
+
const { rows } = await this.#executeQuery(plans.getXminHorizon(lastVacuum, attempted));
|
|
618
|
+
const unreadable = plans.XMIN_HORIZON_QUERY_SOURCES.filter(name => !attempted.includes(name));
|
|
619
|
+
return { row: (rows[0] ?? {}), unreadable };
|
|
620
|
+
}
|
|
621
|
+
catch (err) {
|
|
622
|
+
// A role that cannot read one of the catalogs fails the whole statement. Narrow to the
|
|
623
|
+
// sources that do work rather than losing the check entirely - but remember what was dropped,
|
|
624
|
+
// because "no rows" from an unreadable view must not read as "horizon is healthy".
|
|
625
|
+
const readable = await this.#narrowXminHorizonSources(attempted, lastVacuum);
|
|
626
|
+
this.#xminHorizonSources = readable;
|
|
627
|
+
if (!readable.length) {
|
|
628
|
+
this.emit(events.warning, {
|
|
629
|
+
message: 'Unable to read the transaction horizon; the xmin_horizon check is disabled for this instance.',
|
|
630
|
+
data: { type: WARNING_TYPES.XMIN_HORIZON, error: err?.message }
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
return null;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
// Probe each source alone to find which ones this role can actually read. Runs only after the
|
|
637
|
+
// combined query has already failed, so the cost is paid once per instance, not per pass.
|
|
638
|
+
async #narrowXminHorizonSources(attempted, lastVacuum) {
|
|
639
|
+
const readable = [];
|
|
640
|
+
for (const source of attempted) {
|
|
641
|
+
try {
|
|
642
|
+
await this.#executeQuery(plans.getXminHorizon(lastVacuum, [source]));
|
|
643
|
+
readable.push(source);
|
|
644
|
+
}
|
|
645
|
+
catch {
|
|
646
|
+
// Unreadable for this role; reported alongside the warning as unreadableSources.
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
return readable;
|
|
650
|
+
}
|
|
244
651
|
// DDL runs outside the slow-query timer. A REINDEX is expected to take seconds — routing it
|
|
245
652
|
// through #executeQuery would emit a bogus slow_query warning on every rebuild.
|
|
246
653
|
async #executeDdl(sql) {
|
package/dist/manager.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../src/manager.ts"],"names":[],"mappings":"AAEA,OAAO,YAAY,MAAM,aAAa,CAAA;AAGtC,OAAO,KAAK,EAAE,MAAM,SAAS,CAAA;AAC7B,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAA;AAEzC,OAAO,KAAK,UAAU,MAAM,iBAAiB,CAAA;AAG7C,OAAO,KAAK,KAAK,MAAM,YAAY,CAAA;AACnC,OAAO,MAAM,MAAM,aAAa,CAAA;AAChC,OAAO,EAAU,KAAK,eAAe,EAAE,MAAM,UAAU,CAAA;
|
|
1
|
+
{"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../src/manager.ts"],"names":[],"mappings":"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"}
|