pg-boss 12.33.7 → 12.35.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 -2
- package/dist/activity.d.ts +15 -0
- package/dist/activity.d.ts.map +1 -0
- package/dist/activity.js +95 -0
- package/dist/attorney.d.ts +1 -1
- package/dist/attorney.d.ts.map +1 -1
- package/dist/attorney.js +8 -2
- package/dist/boss.d.ts.map +1 -1
- package/dist/boss.js +50 -13
- package/dist/claimTimer.d.ts.map +1 -1
- package/dist/claimTimer.js +19 -1
- package/dist/cli.js +3 -3
- package/dist/clock.d.ts +12 -3
- package/dist/clock.d.ts.map +1 -1
- package/dist/clock.js +71 -26
- package/dist/contractor.d.ts +3 -0
- package/dist/contractor.d.ts.map +1 -1
- package/dist/contractor.js +22 -8
- package/dist/drifter.d.ts +2 -0
- package/dist/drifter.d.ts.map +1 -1
- package/dist/drifter.js +5 -3
- package/dist/index.d.ts +8 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -6
- package/dist/manager.d.ts +7 -5
- package/dist/manager.d.ts.map +1 -1
- package/dist/manager.js +273 -92
- package/dist/migrationStore.d.ts.map +1 -1
- package/dist/migrationStore.js +166 -3
- package/dist/navigator.d.ts.map +1 -1
- package/dist/navigator.js +4 -0
- package/dist/plans.d.ts +28 -13
- package/dist/plans.d.ts.map +1 -1
- package/dist/plans.js +925 -194
- package/dist/schema.json +196 -2
- package/dist/timekeeper.d.ts +2 -1
- package/dist/timekeeper.d.ts.map +1 -1
- package/dist/timekeeper.js +17 -8
- package/dist/types.d.ts +120 -89
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -36,13 +36,12 @@ readme()
|
|
|
36
36
|
|
|
37
37
|
pg-boss is a job queue built in Node.js on top of PostgreSQL in order to provide background processing and reliable asynchronous execution to Node.js applications.
|
|
38
38
|
|
|
39
|
-
pg-boss relies on Postgres's SKIP LOCKED, a feature built specifically for message queues to resolve record locking challenges inherent with relational databases.
|
|
39
|
+
pg-boss relies on Postgres's SKIP LOCKED, a feature built specifically for message queues to resolve record locking challenges inherent with relational databases. Concurrent workers claim jobs without blocking each other, and job processing gets the safety of guaranteed atomic commits.
|
|
40
40
|
|
|
41
41
|
This will likely cater the most to teams already familiar with the simplicity of relational database semantics and operations (SQL, querying, and backups). It will be especially useful to those already relying on PostgreSQL that want to limit how many systems are required to monitor and support in their architecture.
|
|
42
42
|
|
|
43
43
|
|
|
44
44
|
## Summary
|
|
45
|
-
* Exactly-once job delivery
|
|
46
45
|
* Create jobs in an existing db transaction, including adapters for popular ORMs such as Drizzle, Knex, Kysely, Prisma
|
|
47
46
|
* Backpressure-compatible polling workers, including support for LISTEN/NOTIFY low latency delivery
|
|
48
47
|
* Job dependency workflow orchestration
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { IDatabase } from './types.ts';
|
|
2
|
+
/**
|
|
3
|
+
* The db a tracked one wraps, or the db itself if it is not tracked. What a transactional handler
|
|
4
|
+
* is given, since a handler's statements are not pg-boss's to wait for.
|
|
5
|
+
*/
|
|
6
|
+
export declare function untracked<D extends IDatabase>(db: D): D;
|
|
7
|
+
/**
|
|
8
|
+
* Counts the statements pg-boss has in flight, so a TestClock can let them finish before it moves
|
|
9
|
+
* time on. PgBoss wraps its db with this only when its clock is attachable.
|
|
10
|
+
*/
|
|
11
|
+
export declare function trackActivity<T extends IDatabase>(db: T): {
|
|
12
|
+
db: T;
|
|
13
|
+
idle: () => Promise<boolean>;
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=activity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"activity.d.ts","sourceRoot":"","sources":["../src/activity.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAqB,MAAM,YAAY,CAAA;AAI9D;;;GAGG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS,SAAS,EAAG,EAAE,EAAE,CAAC,GAAG,CAAC,CAExD;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,SAAS,EAAG,EAAE,EAAE,CAAC,GAAG;IAAE,EAAE,EAAE,CAAC,CAAC;IAAC,IAAI,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAA;CAAE,CAmFlG"}
|
package/dist/activity.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { setImmediate } from 'node:timers/promises';
|
|
2
|
+
const originals = new WeakMap();
|
|
3
|
+
/**
|
|
4
|
+
* The db a tracked one wraps, or the db itself if it is not tracked. What a transactional handler
|
|
5
|
+
* is given, since a handler's statements are not pg-boss's to wait for.
|
|
6
|
+
*/
|
|
7
|
+
export function untracked(db) {
|
|
8
|
+
return originals.get(db) ?? db;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Counts the statements pg-boss has in flight, so a TestClock can let them finish before it moves
|
|
12
|
+
* time on. PgBoss wraps its db with this only when its clock is attachable.
|
|
13
|
+
*/
|
|
14
|
+
export function trackActivity(db) {
|
|
15
|
+
let inFlight = 0;
|
|
16
|
+
let started = 0;
|
|
17
|
+
let waiters = [];
|
|
18
|
+
const track = (fn) => async (...args) => {
|
|
19
|
+
inFlight++;
|
|
20
|
+
started++;
|
|
21
|
+
try {
|
|
22
|
+
return await fn(...args);
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
inFlight--;
|
|
26
|
+
if (inFlight === 0) {
|
|
27
|
+
const settled = waiters;
|
|
28
|
+
waiters = [];
|
|
29
|
+
for (const resolve of settled)
|
|
30
|
+
resolve();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
// Bound to the original, so an adapter's private fields still work through the proxy.
|
|
35
|
+
const wrap = (target, overrides) => new Proxy(target, {
|
|
36
|
+
get(t, key) {
|
|
37
|
+
if (Object.hasOwn(overrides, key))
|
|
38
|
+
return overrides[key];
|
|
39
|
+
const value = Reflect.get(t, key);
|
|
40
|
+
return typeof value === 'function' ? value.bind(t) : value;
|
|
41
|
+
},
|
|
42
|
+
// A caller that patches a counted method has usually captured the tracked one to call through.
|
|
43
|
+
// Writing the patch onto the original instead would make the tracked one call the patch forever.
|
|
44
|
+
set(t, key, value) {
|
|
45
|
+
if (Object.hasOwn(overrides, key)) {
|
|
46
|
+
overrides[key] = value;
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
return Reflect.set(t, key, value);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
const wrapDb = (inner) => {
|
|
53
|
+
const overrides = {
|
|
54
|
+
executeSql: track((text, values) => inner.executeSql(text, values))
|
|
55
|
+
};
|
|
56
|
+
// Only when present: callers detect the capability with typeof.
|
|
57
|
+
if (typeof inner.beginTransaction === 'function') {
|
|
58
|
+
overrides.beginTransaction = track(async () => wrapTransaction(await inner.beginTransaction()));
|
|
59
|
+
}
|
|
60
|
+
// pg-boss's own pool runs multi-statement work through this, and it begins the transaction on
|
|
61
|
+
// the original, past the override above, so the whole call is counted as one.
|
|
62
|
+
const withTransaction = inner.withTransaction;
|
|
63
|
+
if (typeof withTransaction === 'function') {
|
|
64
|
+
overrides.withTransaction = track((fn) => withTransaction.call(inner, fn));
|
|
65
|
+
}
|
|
66
|
+
const tracked = wrap(inner, overrides);
|
|
67
|
+
originals.set(tracked, inner);
|
|
68
|
+
return tracked;
|
|
69
|
+
};
|
|
70
|
+
// The open transaction is not counted, only each call on it, or a transactional handler's whole
|
|
71
|
+
// run would read as busy.
|
|
72
|
+
const wrapTransaction = (tx) => wrap(tx, {
|
|
73
|
+
db: wrapDb(tx.db),
|
|
74
|
+
commit: track(() => tx.commit()),
|
|
75
|
+
rollback: track(() => tx.rollback())
|
|
76
|
+
});
|
|
77
|
+
async function idle() {
|
|
78
|
+
let waited = false;
|
|
79
|
+
for (;;) {
|
|
80
|
+
if (inFlight > 0) {
|
|
81
|
+
waited = true;
|
|
82
|
+
await new Promise(resolve => waiters.push(resolve));
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
// A caller whose statement just settled runs on in microtasks: it either arms a timer or
|
|
86
|
+
// starts another statement before this turn ends.
|
|
87
|
+
const seen = started;
|
|
88
|
+
await setImmediate();
|
|
89
|
+
if (inFlight === 0 && started === seen)
|
|
90
|
+
return waited;
|
|
91
|
+
waited = true;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return { db: wrapDb(db), idle };
|
|
95
|
+
}
|
package/dist/attorney.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ declare const POLICY: {
|
|
|
4
4
|
MIN_POLLING_INTERVAL_MS: number;
|
|
5
5
|
MAX_RETENTION_DAYS: number;
|
|
6
6
|
};
|
|
7
|
-
declare const COMPATIBILITY_FLAGS: readonly ["noSkipLocked", "noMultiMutationCte", "noTablePartitioning", "noDeferrableConstraints", "noAdvisoryLocks", "noCoveringIndexes", "noAddColumnBackfill", "noListenNotify", "noIndexProgressView", "noReindex", "noMonitorVacuum", "noTransactionalHeartbeat"];
|
|
7
|
+
declare const COMPATIBILITY_FLAGS: readonly ["noSkipLocked", "noMultiMutationCte", "noTablePartitioning", "noDeferrableConstraints", "noAdvisoryLocks", "noCoveringIndexes", "noAddColumnBackfill", "noListenNotify", "noIndexProgressView", "noReindex", "noMonitorVacuum", "noTransactionalHeartbeat", "inlineTableIndexes"];
|
|
8
8
|
declare function validateQueueArgs(config?: any): void;
|
|
9
9
|
declare function pinZonelessDateTime(value: string): string;
|
|
10
10
|
declare function checkSendArgs(args: any): types.Request;
|
package/dist/attorney.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attorney.d.ts","sourceRoot":"","sources":["../src/attorney.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,KAAK,KAAK,MAAM,YAAY,CAAA;AAExC,QAAA,MAAM,MAAM;;;;CAIX,CAAA;AAKD,QAAA,MAAM,mBAAmB,
|
|
1
|
+
{"version":3,"file":"attorney.d.ts","sourceRoot":"","sources":["../src/attorney.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,KAAK,KAAK,MAAM,YAAY,CAAA;AAExC,QAAA,MAAM,MAAM;;;;CAIX,CAAA;AAKD,QAAA,MAAM,mBAAmB,6RAcf,CAAA;AA+EV,iBAAS,iBAAiB,CAAE,MAAM,GAAE,GAAQ,QAc3C;AAiBD,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,CAgCA;AAqBD,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,CA0B9F;AAgHD,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,mBAAmB,EACnB,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
|
@@ -21,7 +21,8 @@ const COMPATIBILITY_FLAGS = [
|
|
|
21
21
|
'noIndexProgressView',
|
|
22
22
|
'noReindex',
|
|
23
23
|
'noMonitorVacuum',
|
|
24
|
-
'noTransactionalHeartbeat'
|
|
24
|
+
'noTransactionalHeartbeat',
|
|
25
|
+
'inlineTableIndexes'
|
|
25
26
|
];
|
|
26
27
|
// The single source of truth for backend presets, mirrored by test/testHelper.ts.
|
|
27
28
|
const BACKEND_PROFILES = {
|
|
@@ -53,7 +54,11 @@ const BACKEND_PROFILES = {
|
|
|
53
54
|
// serializable lands at a timestamp above the handler transaction's. The completion pg-boss
|
|
54
55
|
// runs inside that transaction then cannot write the same row and the batch dies with a
|
|
55
56
|
// WriteTooOldError. YugabyteDB's snapshot isolation waits instead, so it does NOT set this.
|
|
56
|
-
noTransactionalHeartbeat: true
|
|
57
|
+
noTransactionalHeartbeat: true,
|
|
58
|
+
// Each CREATE INDEX or ADD PRIMARY KEY after a CREATE TABLE is its own schema-change job,
|
|
59
|
+
// run after commit even inside the create transaction: ~0.4-2s apiece, ~15s for a fresh
|
|
60
|
+
// schema. Declared inside CREATE TABLE they cost nothing extra.
|
|
61
|
+
inlineTableIndexes: true
|
|
57
62
|
}
|
|
58
63
|
},
|
|
59
64
|
yugabytedb: {
|
|
@@ -441,6 +446,7 @@ function validateWarningConfig(config) {
|
|
|
441
446
|
assert(!('warningSlowQuerySeconds' in config) || config.warningSlowQuerySeconds >= 1, 'configuration assert: warningSlowQuerySeconds must be at least 1');
|
|
442
447
|
assert(!('warningRetentionDays' in config) || (Number.isInteger(config.warningRetentionDays) && config.warningRetentionDays >= 1), 'configuration assert: warningRetentionDays must be an integer >= 1');
|
|
443
448
|
assert(!('warningRetentionDays' in config) || config.warningRetentionDays <= POLICY.MAX_RETENTION_DAYS, `configuration assert: warningRetentionDays cannot exceed ${POLICY.MAX_RETENTION_DAYS} days`);
|
|
449
|
+
config.warningRetentionDays = config.warningRetentionDays || POLICY.MAX_RETENTION_DAYS;
|
|
444
450
|
}
|
|
445
451
|
// Expands config.backend into the internal compatibility flags. The flags are derived
|
|
446
452
|
// solely from the backend profile. They are not part of the public input, so a
|
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;AAEtC,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;
|
|
1
|
+
{"version":3,"file":"boss.d.ts","sourceRoot":"","sources":["../src/boss.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,MAAM,aAAa,CAAA;AAEtC,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;IAIK,KAAK;IAiBL,IAAI;IAwGJ,SAAS,CAAE,KAAK,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC,WAAW,EAAE,EAAE,OAAO,CAAC,EAAE,KAAK,CAAC,gBAAgB;IA0tBvF;;;;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
|
@@ -123,11 +123,15 @@ class Boss extends EventEmitter {
|
|
|
123
123
|
get maintaining() {
|
|
124
124
|
return !!this.#maintaining;
|
|
125
125
|
}
|
|
126
|
+
// Started with supervise disabled too, which only leaves the timer unarmed, so stop() still
|
|
127
|
+
// reaches a supervise() call on an instance that drives maintenance itself.
|
|
126
128
|
async start() {
|
|
127
129
|
if (this.#stopped) {
|
|
128
130
|
this.#stopping = false;
|
|
129
|
-
|
|
130
|
-
|
|
131
|
+
if (this.#config.supervise) {
|
|
132
|
+
this.#superviseTimer = new ClaimTimer(this.#config.clock, this.#config.superviseIntervalSeconds, () => this.#onSupervise());
|
|
133
|
+
this.#superviseTimer.start();
|
|
134
|
+
}
|
|
131
135
|
this.#stopped = false;
|
|
132
136
|
}
|
|
133
137
|
}
|
|
@@ -167,15 +171,15 @@ class Boss extends EventEmitter {
|
|
|
167
171
|
return result;
|
|
168
172
|
}
|
|
169
173
|
async #onSupervise() {
|
|
174
|
+
// Outside the try, so an attempt that skips does not clear the flag the running pass is
|
|
175
|
+
// holding and stop() waits on. See Timekeeper.onCron().
|
|
176
|
+
if (this.#stopped || this.#maintaining)
|
|
177
|
+
return;
|
|
178
|
+
this.#maintaining = true;
|
|
170
179
|
try {
|
|
171
|
-
if (this.#stopped)
|
|
172
|
-
return;
|
|
173
|
-
if (this.#maintaining)
|
|
174
|
-
return;
|
|
175
180
|
if (this.#config.__test__throw_maint) {
|
|
176
181
|
throw new Error(this.#config.__test__throw_maint);
|
|
177
182
|
}
|
|
178
|
-
this.#maintaining = true;
|
|
179
183
|
if (this.#config.__test__delay_maint_ms) {
|
|
180
184
|
await delay(this.#config.__test__delay_maint_ms);
|
|
181
185
|
}
|
|
@@ -213,8 +217,22 @@ class Boss extends EventEmitter {
|
|
|
213
217
|
: plans.dropOldQueueStatsPartitions(this.#config.schema, this.#config.queueStatRetentionDays);
|
|
214
218
|
await this.#executeQuery(sql);
|
|
215
219
|
}
|
|
220
|
+
// Waits for a pass in flight rather than skipping like #onSupervise, since the caller asked for
|
|
221
|
+
// this one. Holding #maintaining keeps it from overlapping the background pass and makes stop()
|
|
222
|
+
// wait for it, as Navigator.resolveNow() does.
|
|
216
223
|
async supervise(value, options) {
|
|
217
|
-
|
|
224
|
+
while (this.#maintaining) {
|
|
225
|
+
await delay(10);
|
|
226
|
+
}
|
|
227
|
+
if (this.#stopping)
|
|
228
|
+
return;
|
|
229
|
+
this.#maintaining = true;
|
|
230
|
+
try {
|
|
231
|
+
await this.#supervisePass(value, options);
|
|
232
|
+
}
|
|
233
|
+
finally {
|
|
234
|
+
this.#maintaining = false;
|
|
235
|
+
}
|
|
218
236
|
}
|
|
219
237
|
// onClaimsSettled fires once every monitor and maintain claim in the pass has been stamped, which
|
|
220
238
|
// is where the background timer measures its next attempt from. Only #onSupervise passes one.
|
|
@@ -275,7 +293,7 @@ class Boss extends EventEmitter {
|
|
|
275
293
|
async #monitor(table, names) {
|
|
276
294
|
if (this.#stopping)
|
|
277
295
|
return;
|
|
278
|
-
const command = plans.trySetQueueMonitorTime(this.#config.schema, names, this.#config.monitorIntervalSeconds);
|
|
296
|
+
const command = plans.trySetQueueMonitorTime(this.#config.schema, names, this.#config.monitorIntervalSeconds, plans.queueClaimSkipLocked(this.#config.backend, this.#config.noSkipLocked));
|
|
279
297
|
const { rows } = await this.#executeQuery(command);
|
|
280
298
|
if (this.#stopping)
|
|
281
299
|
return;
|
|
@@ -288,7 +306,9 @@ class Boss extends EventEmitter {
|
|
|
288
306
|
// statement, not of a queue, so every row carries the same value.
|
|
289
307
|
const refreshStats = rows[0].refreshStats !== false;
|
|
290
308
|
if (refreshStats) {
|
|
291
|
-
|
|
309
|
+
// A pass needs both timers, so passes are as far apart as the longer of the two.
|
|
310
|
+
const passSeconds = Math.max(this.#config.monitorIntervalSeconds, this.#config.superviseIntervalSeconds);
|
|
311
|
+
const cacheStatsSql = plans.cacheQueueStats(this.#config.schema, table, queues, this.#config.noAdvisoryLocks, this.#config.persistQueueStats, { resetMax: plans.deltaResetMax(passSeconds) });
|
|
292
312
|
// The pin this pass cost, taken from the server's own clock (see the pinSeconds column in
|
|
293
313
|
// cacheQueueStats) and not from a stopwatch around the call - that would count pool wait,
|
|
294
314
|
// network and event-loop lag, none of which hold the horizon. The client measurement stays as
|
|
@@ -303,9 +323,26 @@ class Boss extends EventEmitter {
|
|
|
303
323
|
if (rowsCacheStats.length) {
|
|
304
324
|
this.#statsElapsedSeconds += pinned || (Date.now() - statsStarted) / 1000;
|
|
305
325
|
}
|
|
306
|
-
|
|
307
|
-
|
|
326
|
+
// Only the queues this pass wrote counts for. Another instance holding the stats lock means
|
|
327
|
+
// nothing was updated, and inserting anyway copied the previous pass's counters into a
|
|
328
|
+
// second snapshot, which the history then counted twice.
|
|
329
|
+
if (this.#config.persistQueueStats && rowsCacheStats.length) {
|
|
330
|
+
const written = rowsCacheStats.map(row => row.name);
|
|
331
|
+
const insertSql = plans.insertQueueStats(this.#config.schema, written, this.#config.noAdvisoryLocks);
|
|
308
332
|
await this.#executeQuery(insertSql);
|
|
333
|
+
// Jobs committed after the pass that counted their window: the aggregate saw more of them
|
|
334
|
+
// across the recorded windows than the snapshots hold. Recount those queues' recent windows
|
|
335
|
+
// and raise the snapshots that fell short. A second scan, so it is paid only when the
|
|
336
|
+
// check fires, and it counts toward the pin like the aggregate does.
|
|
337
|
+
const late = rowsCacheStats.filter(row => row.trueUp).map(row => row.name);
|
|
338
|
+
if (late.length && !this.#stopping) {
|
|
339
|
+
const trueUpStarted = Date.now();
|
|
340
|
+
const trueUpSql = plans.trueUpQueueStats(this.#config.schema, table, late, this.#config.noAdvisoryLocks);
|
|
341
|
+
const { rows: [trued] } = await this.#executeQuery(trueUpSql);
|
|
342
|
+
if (trued) {
|
|
343
|
+
this.#statsElapsedSeconds += Number(trued.pinSeconds) || (Date.now() - trueUpStarted) / 1000;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
309
346
|
}
|
|
310
347
|
if (this.#stopping)
|
|
311
348
|
return;
|
|
@@ -345,7 +382,7 @@ class Boss extends EventEmitter {
|
|
|
345
382
|
async #maintain(table, names) {
|
|
346
383
|
if (this.#stopping)
|
|
347
384
|
return;
|
|
348
|
-
const command = plans.trySetQueueDeletionTime(this.#config.schema, names, this.#config.maintenanceIntervalSeconds);
|
|
385
|
+
const command = plans.trySetQueueDeletionTime(this.#config.schema, names, this.#config.maintenanceIntervalSeconds, plans.queueClaimSkipLocked(this.#config.backend, this.#config.noSkipLocked));
|
|
349
386
|
const { rows } = await this.#executeQuery(command);
|
|
350
387
|
if (this.#stopping)
|
|
351
388
|
return;
|
package/dist/claimTimer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"claimTimer.d.ts","sourceRoot":"","sources":["../src/claimTimer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAc,MAAM,YAAY,CAAA;AAEnD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,qBAAa,UAAU;;
|
|
1
|
+
{"version":3,"file":"claimTimer.d.ts","sourceRoot":"","sources":["../src/claimTimer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAc,MAAM,YAAY,CAAA;AAEnD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,qBAAa,UAAU;;gBAUR,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC;IAMnE,KAAK,IAAK,IAAI;IAOd,IAAI,IAAK,IAAI;IAKb;;;;;;;;;;OAUG;IACH,MAAM,CAAE,EAAE,GAAE,MAAiB,GAAG,IAAI;CA0DrC"}
|
package/dist/claimTimer.js
CHANGED
|
@@ -34,6 +34,7 @@ export class ClaimTimer {
|
|
|
34
34
|
#ms;
|
|
35
35
|
#fn;
|
|
36
36
|
#handle;
|
|
37
|
+
#due = 0;
|
|
37
38
|
#stopped = true;
|
|
38
39
|
#anchored = false;
|
|
39
40
|
constructor(clock, seconds, fn) {
|
|
@@ -72,7 +73,24 @@ export class ClaimTimer {
|
|
|
72
73
|
this.#disarm();
|
|
73
74
|
if (this.#stopped)
|
|
74
75
|
return;
|
|
75
|
-
this.#
|
|
76
|
+
this.#due = this.#clock.now() + ms;
|
|
77
|
+
this.#wait(ms);
|
|
78
|
+
}
|
|
79
|
+
// Node's timers run on the monotonic clock and can fire a millisecond short of the wall clock the
|
|
80
|
+
// claim is measured on. Against a server whose now() has millisecond resolution and sits in the
|
|
81
|
+
// same process (PGlite), a reply inside the millisecond it was stamped leaves no round trip to
|
|
82
|
+
// absorb that, and the claim is refused. Waiting out the remainder keeps the attempt from landing
|
|
83
|
+
// before the wall clock says the wait is over.
|
|
84
|
+
#wait(ms) {
|
|
85
|
+
this.#handle = this.#clock.setTimeout(() => {
|
|
86
|
+
const remaining = this.#due - this.#clock.now();
|
|
87
|
+
if (remaining > 0) {
|
|
88
|
+
this.#wait(remaining);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
this.#run();
|
|
92
|
+
}
|
|
93
|
+
}, ms);
|
|
76
94
|
}
|
|
77
95
|
#disarm() {
|
|
78
96
|
if (this.#handle !== undefined) {
|
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ import { readFileSync, existsSync } from 'node:fs';
|
|
|
4
4
|
import { resolve } from 'node:path';
|
|
5
5
|
import Db from "./db.js";
|
|
6
6
|
import * as plans from "./plans.js";
|
|
7
|
-
import Contractor from "./contractor.js";
|
|
7
|
+
import Contractor, { installConfig } from "./contractor.js";
|
|
8
8
|
import * as migrationStore from "./migrationStore.js";
|
|
9
9
|
import * as attorney from "./attorney.js";
|
|
10
10
|
import packageJson from '../package.json' with { type: 'json' };
|
|
@@ -267,7 +267,7 @@ async function cmdCreate(args) {
|
|
|
267
267
|
return;
|
|
268
268
|
}
|
|
269
269
|
console.log(`Creating pg-boss schema "${schema}"...`);
|
|
270
|
-
const sql = plans.create(schema, schemaVersion, { ...config, createSchema: true });
|
|
270
|
+
const sql = plans.create(schema, schemaVersion, { ...await installConfig(db, config), createSchema: true });
|
|
271
271
|
await db.executeSql(sql);
|
|
272
272
|
console.log(`Successfully created pg-boss schema "${schema}" at version ${schemaVersion}`);
|
|
273
273
|
}
|
|
@@ -315,7 +315,7 @@ async function cmdMigrate(args) {
|
|
|
315
315
|
const version = await getSchemaVersion(db, schema);
|
|
316
316
|
if (version === null) {
|
|
317
317
|
console.log(`pg-boss is not installed. Creating schema "${schema}"...`);
|
|
318
|
-
const sql = plans.create(schema, schemaVersion, { ...config, createSchema: true });
|
|
318
|
+
const sql = plans.create(schema, schemaVersion, { ...await installConfig(db, config), createSchema: true });
|
|
319
319
|
await db.executeSql(sql);
|
|
320
320
|
console.log(`Successfully created pg-boss schema "${schema}" at version ${schemaVersion}`);
|
|
321
321
|
return;
|
package/dist/clock.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export declare function isAttachable(clock: Clock): clock is AttachableClock;
|
|
|
4
4
|
interface Target {
|
|
5
5
|
db: IDatabase;
|
|
6
6
|
schema: string;
|
|
7
|
+
idle?: () => Promise<boolean>;
|
|
7
8
|
}
|
|
8
9
|
/**
|
|
9
10
|
* A clock a test drives by hand. Time only moves through setTime() and tick(); timers only fire
|
|
@@ -18,11 +19,19 @@ export declare class TestClock implements AttachableClock {
|
|
|
18
19
|
setInterval(fn: () => void, ms: number): ClockTimer;
|
|
19
20
|
clearTimeout(handle: ClockTimer): void;
|
|
20
21
|
clearInterval(handle: ClockTimer): void;
|
|
21
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* Jumps to a time, forwards or backwards, firing nothing. Settles first, so work already in
|
|
24
|
+
* flight finishes on the time it started under. Postgres sees a forward jump as elapsed time, so
|
|
25
|
+
* JS timers do too: each one the jump passed fires once on the next tick, as if the process had
|
|
26
|
+
* slept, and an interval then keeps its period from there instead of replaying every period it
|
|
27
|
+
* skipped. A backward jump leaves timers alone, so JS deadlines still agree with the database's.
|
|
28
|
+
*/
|
|
22
29
|
setTime(t: Date | number | string): Promise<void>;
|
|
23
30
|
/**
|
|
24
|
-
* Advances by ms, firing each due timer in order at its own due time.
|
|
25
|
-
*
|
|
31
|
+
* Advances by ms, firing each due timer in order at its own due time. After each timer, and
|
|
32
|
+
* before and after the whole advance, waits until pg-boss has no statement in flight, so a poll
|
|
33
|
+
* the tick fired finishes, and anything it re-arms before the target fires in this same tick.
|
|
34
|
+
* Does not wait for job handlers; observe their effects with spies or by querying.
|
|
26
35
|
*/
|
|
27
36
|
tick(ms: number): Promise<void>;
|
|
28
37
|
attach(target: Target): Promise<AsyncDisposable>;
|
package/dist/clock.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"clock.d.ts","sourceRoot":"","sources":["../src/clock.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAE/E,eAAO,MAAM,WAAW,EAAE,KAMzB,CAAA;AAED,wBAAgB,YAAY,CAAE,KAAK,EAAE,KAAK,GAAG,KAAK,IAAI,eAAe,CAEpE;AASD,UAAU,MAAM;IACd,EAAE,EAAE,SAAS,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"clock.d.ts","sourceRoot":"","sources":["../src/clock.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAE/E,eAAO,MAAM,WAAW,EAAE,KAMzB,CAAA;AAED,wBAAgB,YAAY,CAAE,KAAK,EAAE,KAAK,GAAG,KAAK,IAAI,eAAe,CAEpE;AASD,UAAU,MAAM;IACd,EAAE,EAAE,SAAS,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAA;CAC9B;AAUD;;;;GAIG;AACH,qBAAa,SAAU,YAAW,eAAe;;gBAalC,KAAK,GAAE,IAAI,GAAG,MAAM,GAAG,MAAmB;IAIvD,GAAG,IAAK,MAAM;IAId,UAAU,CAAE,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,GAAG,UAAU;IAInD,WAAW,CAAE,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,GAAG,UAAU;IAIpD,YAAY,CAAE,MAAM,EAAE,UAAU,GAAG,IAAI;IAKvC,aAAa,CAAE,MAAM,EAAE,UAAU,GAAG,IAAI;IAIxC;;;;;;OAMG;IACG,OAAO,CAAE,CAAC,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAqBxD;;;;;OAKG;IACG,IAAI,CAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAiDhC,MAAM,CAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;CAsFxD"}
|
package/dist/clock.js
CHANGED
|
@@ -26,7 +26,9 @@ export class TestClock {
|
|
|
26
26
|
#now;
|
|
27
27
|
#timers = [];
|
|
28
28
|
#seq = 0;
|
|
29
|
-
|
|
29
|
+
// tick() and setTime() both await I/O before they finish, so either one started meanwhile would
|
|
30
|
+
// interleave with the one running.
|
|
31
|
+
#busy = null;
|
|
30
32
|
// Every (db, schema) this clock currently pushes time to. More than one because a multi-instance
|
|
31
33
|
// test shares one clock across several PgBoss instances, each of which attaches on start(). The
|
|
32
34
|
// schema is shared, so the last handle to be disposed is the one that restores job_now() and
|
|
@@ -52,44 +54,77 @@ export class TestClock {
|
|
|
52
54
|
clearInterval(handle) {
|
|
53
55
|
this.clearTimeout(handle);
|
|
54
56
|
}
|
|
55
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* Jumps to a time, forwards or backwards, firing nothing. Settles first, so work already in
|
|
59
|
+
* flight finishes on the time it started under. Postgres sees a forward jump as elapsed time, so
|
|
60
|
+
* JS timers do too: each one the jump passed fires once on the next tick, as if the process had
|
|
61
|
+
* slept, and an interval then keeps its period from there instead of replaying every period it
|
|
62
|
+
* skipped. A backward jump leaves timers alone, so JS deadlines still agree with the database's.
|
|
63
|
+
*/
|
|
56
64
|
async setTime(t) {
|
|
57
|
-
this.#
|
|
58
|
-
|
|
65
|
+
this.#enter('setTime');
|
|
66
|
+
try {
|
|
67
|
+
const next = toMillis(t);
|
|
68
|
+
await this.#settle();
|
|
69
|
+
// #timers is sorted by due time, so the overdue timers are a prefix and stay sorted once moved.
|
|
70
|
+
for (const timer of this.#timers) {
|
|
71
|
+
if (timer.due >= next)
|
|
72
|
+
break;
|
|
73
|
+
timer.due = next;
|
|
74
|
+
}
|
|
75
|
+
this.#now = next;
|
|
76
|
+
await this.#push();
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
this.#busy = null;
|
|
80
|
+
}
|
|
59
81
|
}
|
|
60
82
|
/**
|
|
61
|
-
* Advances by ms, firing each due timer in order at its own due time.
|
|
62
|
-
*
|
|
83
|
+
* Advances by ms, firing each due timer in order at its own due time. After each timer, and
|
|
84
|
+
* before and after the whole advance, waits until pg-boss has no statement in flight, so a poll
|
|
85
|
+
* the tick fired finishes, and anything it re-arms before the target fires in this same tick.
|
|
86
|
+
* Does not wait for job handlers; observe their effects with spies or by querying.
|
|
63
87
|
*/
|
|
64
88
|
async tick(ms) {
|
|
65
|
-
|
|
66
|
-
throw new Error('TestClock: tick() called while a tick is in progress');
|
|
67
|
-
}
|
|
68
|
-
this.#ticking = true;
|
|
89
|
+
this.#enter('tick');
|
|
69
90
|
try {
|
|
91
|
+
await this.#settle();
|
|
70
92
|
const target = this.#now + ms;
|
|
71
|
-
|
|
72
|
-
const timer = this.#timers
|
|
73
|
-
if (timer.due
|
|
74
|
-
this.#
|
|
93
|
+
for (;;) {
|
|
94
|
+
const timer = this.#timers[0];
|
|
95
|
+
if (timer && timer.due <= target) {
|
|
96
|
+
this.#timers.shift();
|
|
97
|
+
if (timer.due > this.#now) {
|
|
98
|
+
this.#now = timer.due;
|
|
99
|
+
await this.#push();
|
|
100
|
+
}
|
|
101
|
+
// Reinsert the same object so the handle handed out by setInterval still clears it.
|
|
102
|
+
if (timer.interval !== null) {
|
|
103
|
+
timer.due += timer.interval;
|
|
104
|
+
timer.seq = this.#seq++;
|
|
105
|
+
this.#insert(timer);
|
|
106
|
+
}
|
|
107
|
+
timer.fn();
|
|
108
|
+
}
|
|
109
|
+
else if (this.#now < target) {
|
|
110
|
+
this.#now = target;
|
|
75
111
|
await this.#push();
|
|
76
112
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
timer.due += timer.interval;
|
|
80
|
-
timer.seq = this.#seq++;
|
|
81
|
-
this.#insert(timer);
|
|
113
|
+
else {
|
|
114
|
+
break;
|
|
82
115
|
}
|
|
83
|
-
|
|
84
|
-
await setImmediate();
|
|
116
|
+
await this.#settle();
|
|
85
117
|
}
|
|
86
|
-
this.#now = target;
|
|
87
|
-
await this.#push();
|
|
88
|
-
await setImmediate();
|
|
89
118
|
}
|
|
90
119
|
finally {
|
|
91
|
-
this.#
|
|
120
|
+
this.#busy = null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
#enter(method) {
|
|
124
|
+
if (this.#busy) {
|
|
125
|
+
throw new Error(`TestClock: ${method}() called while a ${this.#busy} is in progress`);
|
|
92
126
|
}
|
|
127
|
+
this.#busy = method;
|
|
93
128
|
}
|
|
94
129
|
async attach(target) {
|
|
95
130
|
const { db, schema } = target;
|
|
@@ -111,7 +146,7 @@ export class TestClock {
|
|
|
111
146
|
// The session opt-in is not issued here. attach() runs after the contractor has already opened
|
|
112
147
|
// connections and migrated, so a SET on this one session would miss every other one. PgBoss
|
|
113
148
|
// declares it through db.setSessionStatements() before anything opens; see #doStart.
|
|
114
|
-
const entry = { db, schema };
|
|
149
|
+
const entry = { db, schema, idle: target.idle };
|
|
115
150
|
this.#targets.push(entry);
|
|
116
151
|
let disposed = false;
|
|
117
152
|
return {
|
|
@@ -153,4 +188,14 @@ export class TestClock {
|
|
|
153
188
|
const seconds = this.#now / 1000;
|
|
154
189
|
await Promise.all(this.#targets.map(({ db, schema }) => db.executeSql(`UPDATE ${plans.clockTable(schema)} SET now = to_timestamp($1)`, [seconds])));
|
|
155
190
|
}
|
|
191
|
+
// Repeats until every target is quiet in the same pass: one instance going quiet can wake
|
|
192
|
+
// another, for example through a NOTIFY, after that one's idle() has already returned.
|
|
193
|
+
async #settle() {
|
|
194
|
+
for (;;) {
|
|
195
|
+
await setImmediate();
|
|
196
|
+
const waited = await Promise.all(this.#targets.map(t => t.idle ? t.idle() : false));
|
|
197
|
+
if (!waited.includes(true))
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
156
201
|
}
|
package/dist/contractor.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type * as types from './types.ts';
|
|
2
|
+
export declare function installConfig<T extends {
|
|
3
|
+
inlineTableIndexes?: boolean;
|
|
4
|
+
}>(db: types.IDatabase, config: T): Promise<T>;
|
|
2
5
|
declare class Contractor {
|
|
3
6
|
static constructionPlans(schema?: string, options?: types.ConstructionPlanOptions): string;
|
|
4
7
|
static migrationPlans(schema?: string, version?: number, options?: types.MigrationPlanOptions): string;
|
package/dist/contractor.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contractor.d.ts","sourceRoot":"","sources":["../src/contractor.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,KAAK,KAAK,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"contractor.d.ts","sourceRoot":"","sources":["../src/contractor.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,KAAK,KAAK,MAAM,YAAY,CAAA;AAsBxC,wBAAsB,aAAa,CAAC,CAAC,SAAS;IAAE,kBAAkB,CAAC,EAAE,OAAO,CAAA;CAAE,EAAG,EAAE,EAAE,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAO3H;AAED,cAAM,UAAU;IACd,MAAM,CAAC,iBAAiB,CAAE,MAAM,SAAuB,EAAE,OAAO,GAAE,KAAK,CAAC,uBAA4B;IAcpG,MAAM,CAAC,cAAc,CAAE,MAAM,SAAuB,EAAE,OAAO,SAAoB,EAAE,OAAO,GAAE,KAAK,CAAC,oBAAyB;IAS3H,MAAM,CAAC,aAAa,CAAE,MAAM,SAAuB,EAAE,OAAO,SAAgB,EAAE,OAAO,GAAE,KAAK,CAAC,WAAgB;IAM7G,OAAO,CAAC,MAAM,CAAkC;IAChD,OAAO,CAAC,EAAE,CAAiB;IAC3B,OAAO,CAAC,UAAU,CAAmB;gBAExB,EAAE,EAAE,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,0BAA0B;IAgBpE,aAAa;IAKb,WAAW;IAKX,KAAK;YAsBG,yBAAyB;IAkCjC,WAAW,CAAE,OAAO,GAAE;QAAE,aAAa,CAAC,EAAE,OAAO,CAAA;KAAO,GAAG,OAAO,CAAC,KAAK,CAAC,iBAAiB,CAAC;IAwHzF,mBAAmB,IAAK,OAAO,CAAC,OAAO,CAAC;IASxC,oBAAoB,IAAK,OAAO,CAAC,IAAI,CAAC;IAItC,KAAK;IAcL,MAAM;IASN,OAAO,CAAE,OAAO,EAAE,MAAM;IASxB,IAAI,CAAE,OAAO,EAAE,MAAM;IAKrB,QAAQ,CAAE,OAAO,EAAE,MAAM;CAIhC;AAED,eAAe,UAAU,CAAA"}
|
package/dist/contractor.js
CHANGED
|
@@ -17,6 +17,17 @@ function planConfig(schema, backend) {
|
|
|
17
17
|
// value, so `{ backend: undefined }` is a named backend that is not one of the profiles.
|
|
18
18
|
return getConfig(backend ? { schema, backend } : { schema });
|
|
19
19
|
}
|
|
20
|
+
// Whether a live install may use inlineTableIndexes. The flag comes from the backend profile, but
|
|
21
|
+
// the syntax it emits is CockroachDB's own, and the cockroachdb profile is otherwise safe to run on
|
|
22
|
+
// plain Postgres (its other flags only remove features). So a live install asks the server what it
|
|
23
|
+
// is before inlining. Printed plans have no server to ask and follow the named backend instead.
|
|
24
|
+
export async function installConfig(db, config) {
|
|
25
|
+
if (!config.inlineTableIndexes)
|
|
26
|
+
return config;
|
|
27
|
+
const { rows } = await db.executeSql(plans.SERVER_VERSION);
|
|
28
|
+
const cockroach = /cockroachdb/i.test(String(rows[0]?.version ?? ''));
|
|
29
|
+
return cockroach ? config : { ...config, inlineTableIndexes: false };
|
|
30
|
+
}
|
|
20
31
|
class Contractor {
|
|
21
32
|
static constructionPlans(schema = plans.DEFAULT_SCHEMA, options = {}) {
|
|
22
33
|
const { createSchema = true, backend } = options;
|
|
@@ -26,7 +37,8 @@ class Contractor {
|
|
|
26
37
|
noTablePartitioning: config.noTablePartitioning,
|
|
27
38
|
noDeferrableConstraints: config.noDeferrableConstraints,
|
|
28
39
|
noAdvisoryLocks: config.noAdvisoryLocks,
|
|
29
|
-
noCoveringIndexes: config.noCoveringIndexes
|
|
40
|
+
noCoveringIndexes: config.noCoveringIndexes,
|
|
41
|
+
inlineTableIndexes: config.inlineTableIndexes
|
|
30
42
|
});
|
|
31
43
|
}
|
|
32
44
|
static migrationPlans(schema = plans.DEFAULT_SCHEMA, version = schemaVersion - 1, options = {}) {
|
|
@@ -190,10 +202,12 @@ class Contractor {
|
|
|
190
202
|
liveConstraints = [];
|
|
191
203
|
}
|
|
192
204
|
const building = new Set(bamCommands.map(plans.bamCommandIndexName).filter((n) => n !== null));
|
|
193
|
-
// CockroachDB renders column types (INT8 vs integer), default expressions,
|
|
194
|
-
// definitions
|
|
195
|
-
//
|
|
196
|
-
//
|
|
205
|
+
// CockroachDB renders column types (INT8 vs integer), default expressions, constraint definitions,
|
|
206
|
+
// index definitions (`name ASC`, `''::STRING`, `!=`, `IN (...)`) and function bodies (database-
|
|
207
|
+
// qualified names, `::INT8`, `now():::TIMESTAMPTZ`) differently from standard Postgres, so the
|
|
208
|
+
// canonical-form checks would flag every one of them there. Restrict definition drift to
|
|
209
|
+
// Postgres-typed backends; the presence checks (tables, indexes and their validity, column
|
|
210
|
+
// names, functions, enum) still run everywhere.
|
|
197
211
|
const canonicalPg = this.config.backend !== 'cockroachdb';
|
|
198
212
|
const expectedColumns = plans.expectedManagedColumns(schema, partitioned, partitions)
|
|
199
213
|
.map(c => canonicalPg ? c : { table: c.table, columns: c.columns });
|
|
@@ -204,9 +218,9 @@ class Contractor {
|
|
|
204
218
|
const expectedIndexes = plans.expectedManagedIndexes(schema, partitioned, partitions)
|
|
205
219
|
.map(i => this.config.noCoveringIndexes ? { ...i, include: '' } : i);
|
|
206
220
|
return drifter.computeSchemaDrift({
|
|
207
|
-
indexes: { expected: expectedIndexes, live, building },
|
|
221
|
+
indexes: { expected: expectedIndexes, live, building, presenceOnly: !canonicalPg },
|
|
208
222
|
tables: { expected: plans.expectedManagedTables(schema, partitioned, partitions), live: liveTables ?? [...new Set(liveColumns.map(c => c.table))] },
|
|
209
|
-
functions: functionsSupported ? { expected: plans.expectedManagedFunctions(schema, partitioned, options), live: liveFunctions } : undefined,
|
|
223
|
+
functions: functionsSupported ? { expected: plans.expectedManagedFunctions(schema, partitioned, options), live: liveFunctions, presenceOnly: !canonicalPg } : undefined,
|
|
210
224
|
columns: { expected: expectedColumns, live: liveColumns },
|
|
211
225
|
constraints: canonicalPg ? { expected: plans.expectedManagedConstraints(schema, partitioned), live: liveConstraints } : undefined,
|
|
212
226
|
enum: { name: 'job_state', expected: plans.EXPECTED_JOB_STATES, actual: enumLabels }
|
|
@@ -246,7 +260,7 @@ class Contractor {
|
|
|
246
260
|
}
|
|
247
261
|
async create() {
|
|
248
262
|
try {
|
|
249
|
-
const commands = plans.create(this.config.schema, schemaVersion, this.config);
|
|
263
|
+
const commands = plans.create(this.config.schema, schemaVersion, await installConfig(this.db, this.config));
|
|
250
264
|
await this.db.executeSql(commands);
|
|
251
265
|
}
|
|
252
266
|
catch (err) {
|