Skip to content

Commit c02539d

Browse files
committed
Merge upstream/master into feat/schedule-last-job-id
Upstream released 12.30.0 with migration v40 (job fetch index tuning and vacuum monitoring, timgit#885), the same version slot this branch had taken for schedule.last_job_id. Upstream's v40 stands as written. schedule.last_job_id moves to v41, release 12.31.0, with package.json (version, pgboss.schema) and src/schema.json following it. The timekeeper comment about send-it payloads written without a `key` now names 12.31.0 as the release that added the field. The two commits below still read "Migration v40" and "version 12.30.0" in their messages, since they are already published; the migration is v41.
2 parents 6724b4c + 8e4286f commit c02539d

38 files changed

Lines changed: 3247 additions & 860 deletions

docs/api/constructor.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,46 @@ The following configuration options should not normally need to be changed, but
165165
166166
How often the index bloat check runs. One instance per interval performs it, coordinated through the database, so adding instances does not multiply the work. Cannot exceed 24 hours.
167167
168+
* **monitorVacuum**, bool, default true
169+
170+
Whether to check that vacuum is keeping up with the queues. Set `false` to disable.
171+
172+
One measurement, two [`warning`](./events.md#warning) types, because the fixes are opposite:
173+
174+
| warning | what it means | the fix |
175+
| --- | --- | --- |
176+
| `xmin_horizon` | vacuum runs and reclaims nothing | find and release whatever is pinning the horizon |
177+
| `autovacuum_disabled` | nothing is vacuuming the table at all | turn autovacuum back on, or vacuum on a schedule that keeps up |
178+
179+
While something holds the horizon back — a backend sitting in an open transaction, a lagging replication slot, a standby with `hot_standby_feedback` enabled, or a prepared transaction — autovacuum cannot reclaim anything your queues delete. Dead tuples accumulate, indexes bloat, and every later vacuum pass gets more expensive. This is the precondition behind most reports of a Postgres queue degrading over time, and it is invisible from the queue's own counters: a backlog caused by too few workers and a backlog caused by a pinned horizon look identical and have opposite fixes.
180+
181+
There is no threshold to tune, because the check measures the damage rather than guessing at it. It reads `pg_stat_user_tables` for pg-boss's own job tables. Both warnings share a first condition:
182+
183+
1. a job table is past the point Postgres itself would vacuum it — `autovacuum_vacuum_threshold + autovacuum_vacuum_scale_factor × n_live_tup`, honouring per-table storage parameters over the cluster settings.
184+
185+
Two consecutive passes then decide which diagnosis applies. For `xmin_horizon`:
186+
187+
2. a vacuum has since run on that table and the dead-tuple count did not fall, so vacuum tried and reclaimed nothing — this is what separates a pinned horizon from ordinary churn, where a vacuum drops the count sharply;
188+
3. a horizon holder exists that is old enough to explain it — for a backend, one whose transaction was already open when that vacuum ran; for a replication slot, standby or prepared transaction, any at all, since those advertise an xmin only while something is genuinely stuck.
189+
190+
For `autovacuum_disabled`, instead:
191+
192+
2. the table has `autovacuum_enabled = false`, no vacuum ran between the two passes, and the dead-tuple count grew.
193+
194+
Turning autovacuum off on a queue table and vacuuming on your own schedule is a legitimate setup, and it stays quiet: a manual vacuum both moves the timestamp and drops the count, so neither branch matches. The warning is for the case where nothing is running at all.
195+
196+
Sensitivity is therefore tuned with Postgres's own autovacuum settings, per table if you want a particular queue watched more or less closely:
197+
198+
```sql
199+
ALTER TABLE pgboss.job_common SET (autovacuum_vacuum_scale_factor = 0.05);
200+
```
201+
202+
Because the second condition compares two observations, the first supervise pass after a horizon is pinned never warns; the warning arrives on a later pass, once a vacuum has actually failed.
203+
204+
The `xmin_horizon` warning names which holder is responsible so it can be tracked down — start with `pg_stat_activity` for idle-in-transaction backends and `pg_replication_slots` for unread slots. If the connected role cannot read one of those catalogs, the warning's `unreadableSources` lists what could not be checked, so a partial answer is never reported as a clean one.
205+
206+
Not available on CockroachDB or YugabyteDB, which reclaim on their own schedule rather than from the oldest live snapshot.
207+
168208
* **flowIntervalSeconds**, int, default 5 seconds
169209
170210
How often the background flow resolver runs to unblock dependent jobs (created via [`flow()`](./jobs.md#flowjobs-options)) whose parents have completed. Completing a job no longer unblocks its dependents inline; this resolver handles it shortly after, off the completion hot path. Only runs when `supervise` is enabled.

docs/api/events.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ boss.on('warning', ({ message, data }) => {
3333
| `clock_skew` | Database clock is out of sync with application server | `seconds`, `direction` |
3434
| `listen_notify_unavailable` | `useListenNotify` is enabled but a `LISTEN/NOTIFY` listener could not be established (for example a `db` adapter without `listen`, or PgBouncer transaction pooling); pg-boss continues with polling only | `type`, `error` |
3535
| `index_bloat` | A job index is holding far more pages than its live entries need and was not rebuilt — because rebuilds are disabled, the connected role does not own the index, the index exceeds `maxIndexBytes`, or `REINDEX CONCURRENTLY` failed. The message names the reason. Emitted once per index rather than on every pass, and again if the condition returns after being cleared | `name`, `table`, `pages`, `entries`, `bytes`, `owned` |
36+
| `xmin_horizon` | Something is holding the database's MVCC transaction horizon back — an open transaction, a replication slot, a standby with `hot_standby_feedback`, or a prepared transaction — so autovacuum cannot reclaim the rows pg-boss deletes and both tables and indexes grow without bound. Fires on measured evidence: a job table past the point Postgres would vacuum it, a vacuum that has since run and reclaimed nothing, and a holder old enough to explain it (see [`monitorVacuum`](./constructor.md#monitorvacuum)). The message names the holder class and the table it was measured on. Emitted once per episode rather than on every pass, and again if the horizon is pinned a second time | `source`, `holder`, `transactions`, `table`, `tables`, `liveTuples`, `deadTuples`, `budget`, `vacuumAgeSeconds`, `oldestTransactionSeconds`, `unreadableSources` |
37+
| `autovacuum_disabled` | (see [`monitorVacuum`](./constructor.md#monitorvacuum)) A job table has `autovacuum_enabled = false`, holds more dead rows than Postgres's own vacuum point, and is still growing with no vacuum running against it at all. Distinct from `xmin_horizon`, where vacuum does run and cannot reclaim: here the fix is autovacuum rather than a stuck transaction, so no holder is named. Stays quiet for an operator vacuuming on their own schedule, whose manual vacuum both moves the timestamp and drops the count. Emitted once per episode | `table`, `tables`, `liveTuples`, `deadTuples`, `budget`, `vacuumAgeSeconds` |
38+
| `monitor_backoff` | The queue-stats aggregate spent long enough scanning the job table to risk holding autovacuum back, so the next refresh is deferred (see [`monitorVacuum`](./constructor.md#monitorvacuum)). Cached counts are served in the meantime, including to `getQueueStats({ force: true })`; `capturedOn` tells you how old they are. Job expiry and heartbeat failure are unaffected. The fix is to shrink the job table (retention, `deleteAfterSeconds`) or partition its busiest queues | `elapsedSeconds`, `naptimeSeconds`, `backoffSeconds`, `backoffUntil` |
3639
| `invalid_schedule` | A stored schedule could not be evaluated (for example an unusable `timezone` written by an older release) and was skipped for this cron pass; the remaining schedules are unaffected. Emitted once per broken schedule rather than on every pass, and again if the schedule is edited or the instance restarts | `queue`, `key`, `cron`, `timezone` |
3740

3841
### Warning Persistence

docs/api/jobs.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -435,13 +435,13 @@ Returns an array of jobs from a queue
435435

436436
Number of jobs to return
437437

438-
* `priority`, bool, *default: true*
438+
* `priority`, bool**deprecated, ignored since 12.30.0**
439439

440-
If true, allow jobs with a higher priority to be fetched before jobs with lower or no priority
440+
Jobs are always fetched in priority order. This option existed to skip the priority sort for throughput; the fetch index is now ordered to match the fetch, so there is no sort to skip. Setting it `false` was measured roughly 180x *slower* than leaving it alone, because no index leads with `created_on`. Emits a Node `DeprecationWarning` (code `PGBOSS_DEP_FETCH_SORT`) once per option per instancerun with `--trace-deprecation` to find the call siteand will be rejected in the next major.
441441

442-
* `orderByCreatedOn`, bool, *default: true*
442+
* `orderByCreatedOn`, bool**deprecated, ignored since 12.30.0**
443443

444-
If true, jobs are fetched in the order they were created. Set to false to disable this sorting for improved performance when order doesn't matter.
444+
Jobs are always fetched in creation order. Same reasoning: the fetch index now provides that order directly, so disabling it saved nothing measurable.
445445

446446
* `includeMetadata`, bool, *default: false*
447447

docs/api/ops.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ Use this where pg-boss cannot run the rebuild itself — the connected role does
120120
```js
121121
const commands = await boss.getReindexCommands()
122122
// [
123-
// 'REINDEX INDEX CONCURRENTLY pgboss."job_common_i5"',
123+
// 'REINDEX INDEX CONCURRENTLY pgboss."job_common_i11"',
124124
// 'REINDEX INDEX CONCURRENTLY pgboss."job_common_pkey"'
125125
// ]
126126
```

docs/api/workers.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,11 @@ The default options for `work()` is 1 job every 2 seconds.
5151
- **Throwing** from the handler still fails the entire batch, exactly as without `perJobResults`. Use the returned array to express per-job failures; reserve throwing for batch-wide errors.
5252
- Resolving with anything other than an array is treated as a contract violation and fails the whole batch.
5353

54-
* **priority**, bool, *(default=true)*
54+
* **priority**, bool**deprecated, ignored since 12.30.0**
5555

5656
Same as in [`fetch()`](./jobs#fetchname-options)
5757

58-
* **orderByCreatedOn**, bool, *(default=true)*
58+
* **orderByCreatedOn**, bool**deprecated, ignored since 12.30.0**
5959

6060
Same as in [`fetch()`](./jobs#fetchname-options)
6161

docs/dashboard.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,10 +194,18 @@ const boss = new PgBoss({
194194
});
195195
```
196196

197-
Warnings correlate to `warning` events already emitted by pg-boss:
197+
Warnings correlate to [`warning`](./api/events.md#warning) events already emitted by pg-boss:
198198
- `slow_query`: Queries taking longer than expected
199199
- `queue_backlog`: Queues exceeding their warning threshold
200200
- `clock_skew`: Database clock drift detection
201+
- `listen_notify_unavailable`: `useListenNotify` is on but no listener could be established
202+
- `invalid_schedule`: A stored schedule could not be evaluated and was skipped
203+
- `index_bloat`: A job index holds far more pages than its live entries need and was not rebuilt
204+
- `xmin_horizon`: Something is pinning the MVCC horizon, so vacuum reclaims nothing
205+
- `autovacuum_disabled`: Nothing is vacuuming a job table at all
206+
- `monitor_backoff`: The queue-stats aggregate was deferred to keep autovacuum moving
207+
208+
A warning type written by a newer pg-boss core than the dashboard is displayed under its raw name.
201209

202210
## Tech Stack
203211

0 commit comments

Comments
 (0)